Move Envoy Client to client/ subdirectory

This commit is contained in:
2020-07-13 11:37:45 +02:00
parent 0c4d807e41
commit 0309d0d860
105 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,30 @@
package envoy.client;
import javafx.application.Application;
import envoy.client.ui.Startup;
/**
* Triggers application startup.
* <p>
* To allow Maven shading, the main method has to be separated from the
* {@link Startup} class which extends {@link Application}.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Main.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class Main {
/**
* Starts the application.
*
* @param args the command line arguments are processed by the
* client configuration
* @since Envoy Client v0.1-beta
*/
public static void main(String[] args) { Application.launch(Startup.class, args); }
}

View File

@ -0,0 +1,65 @@
package envoy.client.data;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.util.EnvoyLog;
/**
* Stores elements in a queue to process them later.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Cache.java</strong><br>
* Created: <strong>6 Feb 2020</strong><br>
*
* @param <T> the type of cached elements
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public final class Cache<T> implements Consumer<T>, Serializable {
private final Queue<T> elements = new LinkedList<>();
private transient Consumer<T> processor;
private static final Logger logger = EnvoyLog.getLogger(Cache.class);
private static final long serialVersionUID = 0L;
/**
* Adds an element to the cache.
*
* @param element the element to add
* @since Envoy Client v0.3-alpha
*/
@Override
public void accept(T element) {
logger.log(Level.FINE, String.format("Adding element %s to cache", element));
elements.offer(element);
}
@Override
public String toString() { return String.format("Cache[elements=" + elements + "]"); }
/**
* Sets the processor to which cached elements are relayed.
*
* @param processor the processor to set
* @since Envoy Client v0.3-alpha
*/
public void setProcessor(Consumer<T> processor) { this.processor = processor; }
/**
* Relays all cached elements to the processor.
*
* @throws IllegalStateException if the processor is not initialized
* @since Envoy Client v0.3-alpha
*/
public void relay() {
if (processor == null) throw new IllegalStateException("Processor is not defined");
elements.forEach(processor::accept);
elements.clear();
}
}

View File

@ -0,0 +1,66 @@
package envoy.client.data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* Stores a heterogeneous map of {@link Cache} objects with different type
* parameters.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>CacheMap.java</strong><br>
* Created: <strong>09.07.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class CacheMap implements Serializable {
private final Map<Class<?>, Cache<?>> map = new HashMap<>();
private static final long serialVersionUID = 1L;
/**
* Adds a cache to the map.
*
* @param <T> the type accepted by the cache
* @param key the class that maps to the cache
* @param cache the cache to store
* @since Envoy Client v0.1-beta
*/
public <T> void put(Class<T> key, Cache<T> cache) { map.put(key, cache); }
/**
* Returns a cache mapped by a class.
*
* @param <T> the type accepted by the cache
* @param key the class that maps to the cache
* @return the cache
* @since Envoy Client v0.1-beta
*/
public <T> Cache<T> get(Class<T> key) { return (Cache<T>) map.get(key); }
/**
* Returns a cache mapped by a class or any of its subclasses.
*
* @param <T> the type accepted by the cache
* @param key the class that maps to the cache
* @return the cache
* @since Envoy Client v0.1-beta
*/
public <T> Cache<? super T> getApplicable(Class<T> key) {
Cache<? super T> cache = get(key);
if (cache == null)
for (var e : map.entrySet())
if (e.getKey().isAssignableFrom(key))
cache = (Cache<? super T>) e.getValue();
return cache;
}
/**
* @return the map in which the caches are stored
* @since Envoy Client v0.1-beta
*/
public Map<Class<?>, Cache<?>> getMap() { return map; }
}

View File

@ -0,0 +1,153 @@
package envoy.client.data;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import envoy.client.net.WriteProxy;
import envoy.data.*;
import envoy.data.Message.MessageStatus;
import envoy.event.MessageStatusChange;
/**
* Represents a chat between two {@link User}s
* as a list of {@link Message} objects.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Chat.java</strong><br>
* Created: <strong>19 Oct 2019</strong><br>
*
* @author Maximilian K&auml;fer
* @author Leon Hofmeister
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-alpha
*/
public class Chat implements Serializable {
protected final Contact recipient;
protected final List<Message> messages = new ArrayList<>();
protected int unreadAmount;
private static final long serialVersionUID = 1L;
/**
* Provides the list of messages that the recipient receives.
* <p>
* Saves the Messages in the corresponding chat at that Point.
*
* @param recipient the user who receives the messages
* @since Envoy Client v0.1-alpha
*/
public Chat(Contact recipient) {
this.recipient = recipient;
}
@Override
public String toString() { return String.format("Chat[recipient=%s,messages=%d]", recipient, messages.size()); }
/**
* Generates a hash code based on the recipient.
*
* @since Envoy Client v0.1-beta
*/
@Override
public int hashCode() { return Objects.hash(recipient); }
/**
* Tests equality to another object based on the recipient.
*
* @since Envoy Client v0.1-beta
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Chat)) return false;
Chat other = (Chat) obj;
return Objects.equals(recipient, other.recipient);
}
/**
* Sets the status of all chat messages received from the recipient to
* {@code READ} starting from the bottom and stopping once a read message is
* found.
*
* @param writeProxy the write proxy instance used to notify the server about
* the message status changes
* @throws IOException if a {@link MessageStatusChange} could not be
* delivered to the server
* @since Envoy Client v0.3-alpha
*/
public void read(WriteProxy writeProxy) throws IOException {
for (int i = messages.size() - 1; i >= 0; --i) {
final Message m = messages.get(i);
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
else {
m.setStatus(MessageStatus.READ);
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
}
}
unreadAmount = 0;
}
/**
* @return {@code true} if the newest message received in the chat doesn't have
* the status {@code READ}
* @since Envoy Client v0.3-alpha
*/
public boolean isUnread() { return !messages.isEmpty() && messages.get(messages.size() - 1).getStatus() != MessageStatus.READ; }
/**
* Inserts a message at the correct place according to its creation date.
*
* @param message the message to insert
* @since Envoy Client v0.1-beta
*/
public void insert(Message message) {
for (int i = messages.size() - 1; i >= 0; --i)
if (message.getCreationDate().isAfter(messages.get(i).getCreationDate())) {
messages.add(i + 1, message);
return;
}
messages.add(0, message);
}
/**
* Increments the amount of unread messages.
*
* @since Envoy Client v0.1-beta
*/
public void incrementUnreadAmount() { unreadAmount++; }
/**
* @return the amount of unread mesages in this chat
* @since Envoy Client v0.1-beta
*/
public int getUnreadAmount() { return unreadAmount; }
/**
* @return all messages in the current chat
* @since Envoy Client v0.1-beta
*/
public List<Message> getMessages() { return messages; }
/**
* @return the recipient of a message
* @since Envoy Client v0.1-alpha
*/
public Contact getRecipient() { return recipient; }
/**
* @return whether this {@link Chat} points at a {@link User}
* @since Envoy Client v0.1-beta
*/
public boolean isUserChat() { return recipient instanceof User; }
/**
* @return whether this {@link Chat} points at a {@link Group}
* @since Envoy Client v0.1-beta
*/
public boolean isGroupChat() { return recipient instanceof Group; }
}

View File

@ -0,0 +1,115 @@
package envoy.client.data;
import static java.util.function.Function.identity;
import java.io.File;
import java.util.logging.Level;
import envoy.client.ui.Startup;
import envoy.data.Config;
import envoy.data.ConfigItem;
import envoy.data.LoginCredentials;
/**
* Implements a configuration specific to the Envoy Client with default values
* and convenience methods.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ClientConfig.java</strong><br>
* Created: <strong>01.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class ClientConfig extends Config {
private static ClientConfig config;
/**
* @return the singleton instance of the client config
* @since Envoy Client v0.1-beta
*/
public static ClientConfig getInstance() {
if (config == null) config = new ClientConfig();
return config;
}
private ClientConfig() {
items.put("server", new ConfigItem<>("server", "s", identity(), null, true));
items.put("port", new ConfigItem<>("port", "p", Integer::parseInt, null, true));
items.put("localDB", new ConfigItem<>("localDB", "db", File::new, new File("localDB"), true));
items.put("ignoreLocalDB", new ConfigItem<>("ignoreLocalDB", "nodb", Boolean::parseBoolean, false, false));
items.put("homeDirectory", new ConfigItem<>("homeDirectory", "h", File::new, new File(System.getProperty("user.home"), ".envoy"), true));
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", Level::parse, Level.CONFIG, true));
items.put("consoleLevelBarrier", new ConfigItem<>("consoleLevelBarrier", "cb", Level::parse, Level.FINEST, true));
items.put("user", new ConfigItem<>("user", "u", identity()));
items.put("password", new ConfigItem<>("password", "pw", identity()));
}
/**
* @return the host name of the Envoy server
* @since Envoy Client v0.1-alpha
*/
public String getServer() { return (String) items.get("server").get(); }
/**
* @return the port at which the Envoy server is located on the host
* @since Envoy Client v0.1-alpha
*/
public Integer getPort() { return (Integer) items.get("port").get(); }
/**
* @return the local database specific to the client user
* @since Envoy Client v0.1-alpha
*/
public File getLocalDB() { return (File) items.get("localDB").get(); }
/**
* @return {@code true} if the local database is to be ignored
* @since Envoy Client v0.3-alpha
*/
public Boolean isIgnoreLocalDB() { return (Boolean) items.get("ignoreLocalDB").get(); }
/**
* @return the directory in which all local files are saves
* @since Envoy Client v0.2-alpha
*/
public File getHomeDirectory() { return (File) items.get("homeDirectory").get(); }
/**
* @return the minimal {@link Level} to log inside the log file
* @since Envoy Client v0.2-alpha
*/
public Level getFileLevelBarrier() { return (Level) items.get("fileLevelBarrier").get(); }
/**
* @return the minimal {@link Level} to log inside the console
* @since Envoy Client v0.2-alpha
*/
public Level getConsoleLevelBarrier() { return (Level) items.get("consoleLevelBarrier").get(); }
/**
* @return the user name
* @since Envoy Client v0.3-alpha
*/
public String getUser() { return (String) items.get("user").get(); }
/**
* @return the password
* @since Envoy Client v0.3-alpha
*/
public String getPassword() { return (String) items.get("password").get(); }
/**
* @return {@code true} if user name and password are set
* @since Envoy Client v0.3-alpha
*/
public boolean hasLoginCredentials() { return getUser() != null && getPassword() != null; }
/**
* @return login credentials for the specified user name and password, without
* the registration option
* @since Envoy Client v0.3-alpha
*/
public LoginCredentials getLoginCredentials() { return new LoginCredentials(getUser(), getPassword(), false, Startup.VERSION); }
}

View File

@ -0,0 +1,55 @@
package envoy.client.data;
import java.io.IOException;
import java.time.LocalDateTime;
import envoy.client.net.WriteProxy;
import envoy.data.Contact;
import envoy.data.GroupMessage;
import envoy.data.Message.MessageStatus;
import envoy.data.User;
import envoy.event.GroupMessageStatusChange;
/**
* Represents a chat between a user and a group
* as a list of messages.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>GroupChat.java</strong><br>
* Created: <strong>05.07.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupChat extends Chat {
private final User sender;
private static final long serialVersionUID = 1L;
/**
* @param sender the user sending the messages
* @param recipient the group whose members receive the messages
* @since Envoy Client v0.1-beta
*/
public GroupChat(User sender, Contact recipient) {
super(recipient);
this.sender = sender;
}
@Override
public void read(WriteProxy writeProxy) throws IOException {
for (int i = messages.size() - 1; i >= 0; --i) {
final GroupMessage gmsg = (GroupMessage) messages.get(i);
if (gmsg.getSenderID() != sender.getID()) {
if (gmsg.getMemberStatuses().get(sender.getID()) == MessageStatus.READ) break;
else {
gmsg.getMemberStatuses().replace(sender.getID(), MessageStatus.READ);
writeProxy
.writeMessageStatusChange(new GroupMessageStatusChange(gmsg.getID(), MessageStatus.READ, LocalDateTime.now(), sender.getID()));
}
}
}
unreadAmount = 0;
}
}

View File

@ -0,0 +1,205 @@
package envoy.client.data;
import java.util.*;
import envoy.data.*;
import envoy.event.GroupResize;
import envoy.event.MessageStatusChange;
import envoy.event.NameChange;
/**
* Stores information about the current {@link User} and their {@link Chat}s.
* For message ID generation a {@link IDGenerator} is stored as well.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>LocalDB.java</strong><br>
* Created: <strong>3 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public abstract class LocalDB {
protected User user;
protected Map<String, Contact> users = new HashMap<>();
protected List<Chat> chats = new ArrayList<>();
protected IDGenerator idGenerator;
protected CacheMap cacheMap = new CacheMap();
{
cacheMap.put(Message.class, new Cache<>());
cacheMap.put(MessageStatusChange.class, new Cache<>());
}
/**
* Initializes a storage space for a user-specific list of chats.
*
* @since Envoy Client v0.3-alpha
*/
public void initializeUserStorage() {}
/**
* Stores all users. If the client user is specified, their chats will be stored
* as well. The message id generator will also be saved if present.
*
* @throws Exception if the saving process failed
* @since Envoy Client v0.3-alpha
*/
public void save() throws Exception {}
/**
* Loads all user data.
*
* @throws Exception if the loading process failed
* @since Envoy Client v0.3-alpha
*/
public void loadUsers() throws Exception {}
/**
* Loads all data of the client user.
*
* @throws Exception if the loading process failed
* @since Envoy Client v0.3-alpha
*/
public void loadUserData() throws Exception {}
/**
* Loads the ID generator. Any exception thrown during this process is ignored.
*
* @since Envoy Client v0.3-alpha
*/
public void loadIDGenerator() {}
/**
* Synchronizes the contact list of the client user with the chat and user
* storage.
*
* @since Envoy Client v0.1-beta
*/
public void synchronize() {
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), u));
users.put(user.getName(), user);
// Synchronize user status data
for (Contact contact : users.values())
if (contact instanceof User)
getChat(contact.getID()).ifPresent(chat -> { ((User) chat.getRecipient()).setStatus(((User) contact).getStatus()); });
// Create missing chats
user.getContacts()
.stream()
.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
.forEach(chats::add);
}
/**
* @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys
* @since Envoy Client v0.2-alpha
*/
public Map<String, Contact> getUsers() { return users; }
/**
* @return all saved {@link Chat} objects that list the client user as the
* sender
* @since Envoy Client v0.1-alpha
**/
public List<Chat> getChats() { return chats; }
/**
* @param chats the chats to set
*/
public void setChats(List<Chat> chats) { this.chats = chats; }
/**
* @return the {@link User} who initialized the local database
* @since Envoy Client v0.2-alpha
*/
public User getUser() { return user; }
/**
* @param user the user to set
* @since Envoy Client v0.2-alpha
*/
public void setUser(User user) { this.user = user; }
/**
* @return the message ID generator
* @since Envoy Client v0.3-alpha
*/
public IDGenerator getIDGenerator() { return idGenerator; }
/**
* @param idGenerator the message ID generator to set
* @since Envoy Client v0.3-alpha
*/
public void setIDGenerator(IDGenerator idGenerator) { this.idGenerator = idGenerator; }
/**
* @return {@code true} if an {@link IDGenerator} is present
* @since Envoy Client v0.3-alpha
*/
public boolean hasIDGenerator() { return idGenerator != null; }
/**
* @return the cache map for messages and message status changes
* @since Envoy Client v0.1-beta
*/
public CacheMap getCacheMap() { return cacheMap; }
/**
* Searches for a message by ID.
*
* @param id the ID of the message to search for
* @return an optional containing the message
* @since Envoy Client v0.1-beta
*/
public Optional<Message> getMessage(long id) {
return chats.stream().map(Chat::getMessages).flatMap(List::stream).filter(m -> m.getID() == id).findAny();
}
/**
* Searches for a chat by recipient ID.
*
* @param recipientID the ID of the chat's recipient
* @return an optional containing the chat
* @since Envoy Client v0.1-beta
*/
public Optional<Chat> getChat(long recipientID) { return chats.stream().filter(c -> c.getRecipient().getID() == recipientID).findAny(); }
/**
* Performs a contact name change if the corresponding contact is present.
*
* @param event the {@link NameChange} to process
* @since Envoy Client v0.1-beta
*/
public void replaceContactName(NameChange event) {
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == event.getID()).findAny().ifPresent(c -> c.setName(event.get()));
}
/**
* Performs a group resize operation if the corresponding group is present.
*
* @param event the {@link GroupResize} to process
* @since Envoy Client v0.1-beta
*/
public void updateGroup(GroupResize event) {
chats.stream()
.map(Chat::getRecipient)
.filter(Group.class::isInstance)
.filter(g -> g.getID() == event.getGroupID() && g.getID() != user.getID())
.map(Group.class::cast)
.findAny()
.ifPresent(group -> {
switch (event.getOperation()) {
case ADD:
group.getContacts().add(event.get());
break;
case REMOVE:
group.getContacts().remove(event.get());
break;
}
});
}
}

View File

@ -0,0 +1,88 @@
package envoy.client.data;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import envoy.data.IDGenerator;
import envoy.util.SerializationUtils;
/**
* Implements a {@link LocalDB} in a way that stores all information inside a
* folder on the local file system.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>PersistentLocalDB.java</strong><br>
* Created: <strong>27.10.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-alpha
*/
public final class PersistentLocalDB extends LocalDB {
private File dbDir, userFile, idGeneratorFile, usersFile;
/**
* Constructs an empty local database. To serialize any user-specific data to
* the file system, call {@link PersistentLocalDB#initializeUserStorage()} first
* and then {@link PersistentLocalDB#save()}.
*
* @param dbDir the directory in which to persist data
* @throws IOException if {@code dbDir} is a file (and not a directory)
* @since Envoy Client v0.1-alpha
*/
public PersistentLocalDB(File dbDir) throws IOException {
this.dbDir = dbDir;
// Test if the database directory is actually a directory
if (dbDir.exists() && !dbDir.isDirectory())
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
// Initialize global files
idGeneratorFile = new File(dbDir, "id_gen.db");
usersFile = new File(dbDir, "users.db");
}
/**
* Creates a database file for a user-specific list of chats.
*
* @throws IllegalStateException if the client user is not specified
* @since Envoy Client v0.1-alpha
*/
@Override
public void initializeUserStorage() {
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
userFile = new File(dbDir, user.getID() + ".db");
}
@Override
public void save() throws IOException {
// Save users
SerializationUtils.write(usersFile, users);
// Save user data
if (user != null) SerializationUtils.write(userFile, chats, cacheMap);
// Save id generator
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
}
@Override
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
@Override
public void loadUserData() throws ClassNotFoundException, IOException {
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
chats = (ArrayList<Chat>) in.readObject();
cacheMap = (CacheMap) in.readObject();
}
}
@Override
public void loadIDGenerator() {
try {
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
} catch (ClassNotFoundException | IOException e) {}
}
}

View File

@ -0,0 +1,146 @@
package envoy.client.data;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.prefs.Preferences;
import envoy.util.SerializationUtils;
/**
* Manages all application settings, which are different objects that can be
* changed during runtime and serialized them by using either the file system or
* the {@link Preferences} API.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Settings.java</strong><br>
* Created: <strong>11 Nov 2019</strong><br>
*
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class Settings {
// Actual settings accessible by the rest of the application
private Map<String, SettingsItem<?>> items;
/**
* Settings are stored in this file.
*/
private static final File settingsFile = new File(ClientConfig.getInstance().getHomeDirectory(), "settings.ser");
/**
* Singleton instance of this class.
*/
private static Settings settings = new Settings();
/**
* The way to instantiate the settings. Is set to private to deny other
* instances of that object.
*
* @since Envoy Client v0.2-alpha
*/
private Settings() {
// Load settings from settings file
try {
items = SerializationUtils.read(settingsFile, HashMap.class);
} catch (ClassNotFoundException | IOException e) {
items = new HashMap<>();
}
supplementDefaults();
}
/**
* This method is used to ensure that there is only one instance of Settings.
*
* @return the instance of Settings
* @since Envoy Client v0.2-alpha
*/
public static Settings getInstance() { return settings; }
/**
* Updates the preferences when the save button is clicked.
*
* @throws IOException if an error occurs while saving the themes
* @since Envoy Client v0.2-alpha
*/
public void save() throws IOException {
// Save settings to settings file
SerializationUtils.write(settingsFile, items);
}
private void supplementDefaults() {
items.putIfAbsent("enterToSend", new SettingsItem<>(true, "Enter to send", "Sends a message by pressing the enter key."));
items.putIfAbsent("onCloseMode", new SettingsItem<>(true, "Hide on close", "Hides the chat window when it is closed."));
items.putIfAbsent("currentTheme", new SettingsItem<>("dark", "Current Theme Name", "The name of the currently selected theme."));
}
/**
* @return the name of the currently active theme
* @since Envoy Client v0.2-alpha
*/
public String getCurrentTheme() { return (String) items.get("currentTheme").get(); }
/**
* Sets the name of the current theme.
*
* @param themeName the name to set
* @since Envoy Client v0.2-alpha
*/
public void setCurrentTheme(String themeName) { ((SettingsItem<String>) items.get("currentTheme")).set(themeName); }
/**
* @return true if the currently used theme is one of the default themes
* @since Envoy Client v0.1-beta
*/
public boolean isUsingDefaultTheme() {
final var theme = getCurrentTheme();
return theme.equals("dark") || theme.equals("light");
}
/**
* @return {@code true}, if pressing the {@code Enter} key suffices to send a
* message. Otherwise it has to be pressed in conjunction with the
* {@code Control} key.
* @since Envoy Client v0.2-alpha
*/
public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
/**
* Changes the keystrokes performed by the user to send a message.
*
* @param enterToSend If set to {@code true} a message can be sent by pressing
* the {@code Enter} key. Otherwise it has to be pressed in
* conjunction with the {@code Control} key.
* @since Envoy Client v0.2-alpha
*/
public void setEnterToSend(boolean enterToSend) { ((SettingsItem<Boolean>) items.get("enterToSend")).set(enterToSend); }
/**
* @return the current on close mode.
* @since Envoy Client v0.3-alpha
*/
public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
/**
* Sets the current on close mode.
*
* @param currentOnCloseMode the on close mode that should be set.
* @since Envoy Client v0.3-alpha
*/
public void setCurrentOnCloseMode(boolean currentOnCloseMode) { ((SettingsItem<Boolean>) items.get("onCloseMode")).set(currentOnCloseMode); }
/**
* @return the items
*/
public Map<String, SettingsItem<?>> getItems() { return items; }
/**
* @param items the items to set
*/
public void setItems(Map<String, SettingsItem<?>> items) { this.items = items; }
}

View File

@ -0,0 +1,99 @@
package envoy.client.data;
import java.io.Serializable;
import java.util.function.Consumer;
import javax.swing.JComponent;
/**
* Encapsulates a persistent value that is directly or indirectly mutable by the
* user.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsItem.java</strong><br>
* Created: <strong>23.12.2019</strong><br>
*
* @param <T> the type of this {@link SettingsItem}'s value
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class SettingsItem<T> implements Serializable {
private T value;
private String userFriendlyName, description;
private transient Consumer<T> changeHandler;
private static final long serialVersionUID = 1L;
/**
* Initializes a {@link SettingsItem}. The default value's class will be mapped
* to a {@link JComponent} that can be used to display this {@link SettingsItem}
* to the user.
*
* @param value the default value
* @param userFriendlyName the user friendly name (short)
* @param description the description (long)
* @since Envoy Client v0.3-alpha
*/
public SettingsItem(T value, String userFriendlyName, String description) {
this.value = value;
this.userFriendlyName = userFriendlyName;
this.description = description;
}
/**
* @return the value
* @since Envoy Client v0.3-alpha
*/
public T get() { return value; }
/**
* Changes the value of this {@link SettingsItem}. If a {@code ChangeHandler} if
* defined, it will be invoked with this value.
*
* @param value the value to set
* @since Envoy Client v0.3-alpha
*/
public void set(T value) {
if (changeHandler != null && value != this.value) changeHandler.accept(value);
this.value = value;
}
/**
* @return the userFriendlyName
* @since Envoy Client v0.3-alpha
*/
public String getUserFriendlyName() { return userFriendlyName; }
/**
* @param userFriendlyName the userFriendlyName to set
* @since Envoy Client v0.3-alpha
*/
public void setUserFriendlyName(String userFriendlyName) { this.userFriendlyName = userFriendlyName; }
/**
* @return the description
* @since Envoy Client v0.3-alpha
*/
public String getDescription() { return description; }
/**
* @param description the description to set
* @since Envoy Client v0.3-alpha
*/
public void setDescription(String description) { this.description = description; }
/**
* Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be
* invoked with the current value once during the registration and every time
* when the value changes.
*
* @param changeHandler the changeHandler to set
* @since Envoy Client v0.3-alpha
*/
public void setChangeHandler(Consumer<T> changeHandler) {
this.changeHandler = changeHandler;
changeHandler.accept(value);
}
}

View File

@ -0,0 +1,15 @@
package envoy.client.data;
/**
* Implements a {@link LocalDB} in a way that does not persist any information
* after application shutdown.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>TransientLocalDB.java</strong><br>
* Created: <strong>3 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public final class TransientLocalDB extends LocalDB {
}

View 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);
}
}
}

View 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) {}
}
}

View 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;

View File

@ -0,0 +1,9 @@
/**
* This package contains all data classes and classes related to persistence.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
package envoy.client.data;

View File

@ -0,0 +1,22 @@
package envoy.client.event;
import envoy.data.Message;
import envoy.event.Event;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageCreationEvent.java</strong><br>
* Created: <strong>4 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class MessageCreationEvent extends Event<Message> {
private static final long serialVersionUID = 0L;
/**
* @param message the {@link Message} that has been created
*/
public MessageCreationEvent(Message message) { super(message); }
}

View File

@ -0,0 +1,22 @@
package envoy.client.event;
import envoy.data.Message;
import envoy.event.Event;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageModificationEvent.java</strong><br>
* Created: <strong>4 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class MessageModificationEvent extends Event<Message> {
private static final long serialVersionUID = 0L;
/**
* @param message the {@link Message} that has been modified
*/
public MessageModificationEvent(Message message) { super(message); }
}

View File

@ -0,0 +1,22 @@
package envoy.client.event;
import envoy.event.Event;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SendEvent.java</strong><br>
* Created: <strong>11.02.2020</strong><br>
*
* @author: Maximilian K&aumlfer
* @since Envoy Client v0.3-alpha
*/
public class SendEvent extends Event<Event<?>> {
private static final long serialVersionUID = 0L;
/**
* @param value the event to send to the server
*/
public SendEvent(Event<?> value) { super(value); }
}

View File

@ -0,0 +1,25 @@
package envoy.client.event;
import envoy.event.Event;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ThemeChangeEvent.java</strong><br>
* Created: <strong>15 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class ThemeChangeEvent extends Event<String> {
private static final long serialVersionUID = 0L;
/**
* Initializes a {@link ThemeChangeEvent} conveying information about the change
* of the theme currently in use.
*
* @param theme the name of the new theme
* @since Envoy Client v0.2-alpha
*/
public ThemeChangeEvent(String theme) { super(theme); }
}

View File

@ -0,0 +1,9 @@
/**
* This package contains all client-sided events.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
package envoy.client.event;

View File

@ -0,0 +1,241 @@
package envoy.client.net;
import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.client.data.*;
import envoy.client.event.SendEvent;
import envoy.data.*;
import envoy.event.*;
import envoy.event.contact.ContactOperation;
import envoy.event.contact.ContactSearchResult;
import envoy.util.EnvoyLog;
import envoy.util.SerializationUtils;
/**
* Establishes a connection to the server, performs a handshake and delivers
* certain objects to the server.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Client.java</strong><br>
* Created: <strong>28 Sep 2019</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @author Leon Hofmeister
* @since Envoy Client v0.1-alpha
*/
public class Client implements Closeable {
// Connection handling
private Socket socket;
private Receiver receiver;
private boolean online;
// Asynchronously initialized during handshake
private volatile User sender;
private volatile boolean rejected;
// Configuration, logging and event management
private static final ClientConfig config = ClientConfig.getInstance();
private static final Logger logger = EnvoyLog.getLogger(Client.class);
private static final EventBus eventBus = EventBus.getInstance();
/**
* Enters the online mode by acquiring a user ID from the server. As a
* connection has to be established and a handshake has to be made, this method
* will block for up to 5 seconds. If the handshake does exceed this time limit,
* an exception is thrown.
*
* @param credentials the login credentials of the user
* @param cacheMap the map of all caches needed
* @throws TimeoutException if the server could not be reached
* @throws IOException if the login credentials could not be written
* @throws InterruptedException if the current thread is interrupted while
* waiting for the handshake response
*/
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException {
if (online) throw new IllegalStateException("Handshake has already been performed successfully");
// Establish TCP connection
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
socket = new Socket(config.getServer(), config.getPort());
logger.log(Level.FINE, "Successfully established TCP connection to server");
// Create object receiver
receiver = new Receiver(socket.getInputStream());
// Register user creation processor, contact list processor and message cache
receiver.registerProcessor(User.class, sender -> this.sender = sender);
receiver.registerProcessors(cacheMap.getMap());
receiver.registerProcessor(HandshakeRejection.class, evt -> { rejected = true; eventBus.dispatch(evt); });
rejected = false;
// Start receiver
receiver.start();
// Write login credentials
SerializationUtils.writeBytesWithLength(credentials, socket.getOutputStream());
// Wait for a maximum of five seconds to acquire the sender object
final long start = System.currentTimeMillis();
while (sender == null) {
// Quit immediately after handshake rejection
// This method can then be called again
if (rejected) {
socket.close();
receiver.removeAllProcessors();
return;
}
if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds");
Thread.sleep(500);
}
online = true;
logger.log(Level.INFO, "Handshake completed.");
}
/**
* Initializes the {@link Receiver} used to process data sent from the server to
* this client.
*
* @param localDB the local database used to persist the current
* {@link IDGenerator}
* @param cacheMap the map of all caches needed
* @throws IOException if no {@link IDGenerator} is present and none could be
* requested from the server
* @since Envoy Client v0.2-alpha
*/
public void initReceiver(LocalDB localDB, CacheMap cacheMap) throws IOException {
checkOnline();
// Remove all processors as they are only used during the handshake
receiver.removeAllProcessors();
// Process incoming messages
final var receivedMessageProcessor = new ReceivedMessageProcessor();
final var receivedGroupMessageProcessor = new ReceivedGroupMessageProcessor();
final var messageStatusChangeProcessor = new MessageStatusChangeProcessor();
final var groupMessageStatusChangeProcessor = new GroupMessageStatusChangeProcessor();
receiver.registerProcessor(GroupMessage.class, receivedGroupMessageProcessor);
receiver.registerProcessor(Message.class, receivedMessageProcessor);
receiver.registerProcessor(MessageStatusChange.class, messageStatusChangeProcessor);
receiver.registerProcessor(GroupMessageStatusChange.class, groupMessageStatusChangeProcessor);
// Relay cached messages and message status changes
cacheMap.get(Message.class).setProcessor(receivedMessageProcessor);
cacheMap.get(GroupMessage.class).setProcessor(receivedGroupMessageProcessor);
cacheMap.get(MessageStatusChange.class).setProcessor(messageStatusChangeProcessor);
cacheMap.get(GroupMessageStatusChange.class).setProcessor(groupMessageStatusChangeProcessor);
// Process user status changes
receiver.registerProcessor(UserStatusChange.class, eventBus::dispatch);
// Process message ID generation
receiver.registerProcessor(IDGenerator.class, localDB::setIDGenerator);
// Process name changes
receiver.registerProcessor(NameChange.class, evt -> { localDB.replaceContactName(evt); eventBus.dispatch(evt); });
// Process contact searches
receiver.registerProcessor(ContactSearchResult.class, eventBus::dispatch);
// Process contact operations
receiver.registerProcessor(ContactOperation.class, eventBus::dispatch);
// Process group size changes
receiver.registerProcessor(GroupResize.class, evt -> { localDB.updateGroup(evt); eventBus.dispatch(evt); });
// Send event
eventBus.register(SendEvent.class, evt -> {
try {
sendEvent(evt.get());
} catch (final IOException e) {
logger.log(Level.WARNING, "An error occurred when trying to send " + evt, e);
}
});
// Request a generator if none is present or the existing one is consumed
if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) requestIdGenerator();
// Relay caches
cacheMap.getMap().values().forEach(Cache::relay);
}
/**
* Sends a message to the server. The message's status will be incremented once
* it was delivered successfully.
*
* @param message the message to send
* @throws IOException if the message does not reach the server
* @since Envoy Client v0.3-alpha
*/
public void sendMessage(Message message) throws IOException {
writeObject(message);
message.nextStatus();
}
/**
* Sends an event to the server.
*
* @param evt the event to send
* @throws IOException if the event did not reach the server
*/
public void sendEvent(Event<?> evt) throws IOException { writeObject(evt); }
/**
* Requests a new {@link IDGenerator} from the server.
*
* @throws IOException if the request does not reach the server
* @since Envoy Client v0.3-alpha
*/
public void requestIdGenerator() throws IOException {
logger.log(Level.INFO, "Requesting new id generator...");
writeObject(new IDGeneratorRequest());
}
@Override
public void close() throws IOException { if (online) socket.close(); }
private void writeObject(Object obj) throws IOException {
checkOnline();
logger.log(Level.FINE, "Sending " + obj);
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
}
private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); }
/**
* @return the {@link User} as which this client is logged in
* @since Envoy Client v0.1-alpha
*/
public User getSender() { return sender; }
/**
* Sets the client user which is used to send messages.
*
* @param clientUser the client user to set
* @since Envoy Client v0.2-alpha
*/
public void setSender(User clientUser) { sender = clientUser; }
/**
* @return the {@link Receiver} used by this {@link Client}
*/
public Receiver getReceiver() { return receiver; }
/**
* @return {@code true} if a connection to the server could be established
* @since Envoy Client v0.2-alpha
*/
public boolean isOnline() { return online; }
}

View File

@ -0,0 +1,29 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.event.GroupMessageStatusChange;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>GroupMessageStatusChangePocessor.java</strong><br>
* Created: <strong>03.07.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupMessageStatusChangeProcessor implements Consumer<GroupMessageStatusChange> {
private static final Logger logger = EnvoyLog.getLogger(GroupMessageStatusChangeProcessor.class);
@Override
public void accept(GroupMessageStatusChange evt) {
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid group message status change " + evt);
else EventBus.getInstance().dispatch(evt);
}
}

View File

@ -0,0 +1,35 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageStatusChangeProcessor.java</strong><br>
* Created: <strong>4 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class MessageStatusChangeProcessor implements Consumer<MessageStatusChange> {
private static final Logger logger = EnvoyLog.getLogger(MessageStatusChangeProcessor.class);
/**
* Dispatches a {@link MessageStatusChange} if the status is
* {@code RECEIVED} or {@code READ}.
*
* @param evt the status change event
* @since Envoy Client v0.3-alpha
*/
@Override
public void accept(MessageStatusChange evt) {
if (evt.get().ordinal() < MessageStatus.RECEIVED.ordinal()) logger.warning("Received invalid message status change " + evt);
else EventBus.getInstance().dispatch(evt);
}
}

View File

@ -0,0 +1,33 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Logger;
import envoy.client.event.MessageCreationEvent;
import envoy.data.GroupMessage;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ReceivedGroupMessageProcessor.java</strong><br>
* Created: <strong>13.06.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class ReceivedGroupMessageProcessor implements Consumer<GroupMessage> {
private static final Logger logger = EnvoyLog.getLogger(ReceivedGroupMessageProcessor.class);
@Override
public void accept(GroupMessage groupMessage) {
if (groupMessage.getStatus() == MessageStatus.WAITING || groupMessage.getStatus() == MessageStatus.READ)
logger.warning("The groupMessage has the unexpected status " + groupMessage.getStatus());
// Dispatch event
EventBus.getInstance().dispatch(new MessageCreationEvent(groupMessage));
}
}

View File

@ -0,0 +1,36 @@
package envoy.client.net;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.client.event.MessageCreationEvent;
import envoy.data.Message;
import envoy.data.Message.MessageStatus;
import envoy.event.EventBus;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ReceivedMessageProcessor.java</strong><br>
* Created: <strong>31.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class ReceivedMessageProcessor implements Consumer<Message> {
private static final Logger logger = EnvoyLog.getLogger(ReceivedMessageProcessor.class);
@Override
public void accept(Message message) {
if (message.getStatus() != MessageStatus.SENT) logger.log(Level.WARNING, "The message has the unexpected status " + message.getStatus());
else {
// Update status to RECEIVED
message.nextStatus();
// Dispatch event
EventBus.getInstance().dispatch(new MessageCreationEvent(message));
}
}
}

View File

@ -0,0 +1,118 @@
package envoy.client.net;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.util.EnvoyLog;
import envoy.util.SerializationUtils;
/**
* Receives objects from the server and passes them to processor objects based
* on their class.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Receiver.java</strong><br>
* Created: <strong>30.12.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class Receiver extends Thread {
private final InputStream in;
private final Map<Class<?>, Consumer<?>> processors = new HashMap<>();
private static final Logger logger = EnvoyLog.getLogger(Receiver.class);
/**
* Creates an instance of {@link Receiver}.
*
* @param in the {@link InputStream} to parse objects from
* @since Envoy Client v0.3-alpha
*/
public Receiver(InputStream in) {
super("Receiver");
this.in = in;
}
/**
* Starts the receiver loop. When an object is read, it is passed to the
* appropriate processor.
*
* @since Envoy Client v0.3-alpha
*/
@Override
public void run() {
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];
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();
logger.log(Level.FINE, "Received " + obj);
// 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 %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
* object of the accepted class has been received.
*
* @param processorClass the object class accepted by the processor
* @param processor the object processor
* @since Envoy Client v0.3-alpha
*/
public <T> void registerProcessor(Class<T> processorClass, Consumer<T> processor) { processors.put(processorClass, processor); }
/**
* Adds a map of object processors to this {@link Receiver}.
*
* @param processors the processors to add the processors to add
* @since Envoy Client v0.1-beta
*/
public void registerProcessors(Map<Class<?>, ? extends Consumer<?>> processors) { this.processors.putAll(processors); }
/**
* Removes all object processors registered at this {@link Receiver}.
*
* @since Envoy Client v0.3-alpha
*/
public void removeAllProcessors() { processors.clear(); }
}

View File

@ -0,0 +1,100 @@
package envoy.client.net;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import envoy.client.data.Cache;
import envoy.client.data.LocalDB;
import envoy.data.Message;
import envoy.event.MessageStatusChange;
import envoy.util.EnvoyLog;
/**
* Implements methods to send {@link Message}s and
* {@link MessageStatusChange}s to the server or cache them inside a
* {@link LocalDB} depending on the online status.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>WriteProxy.java</strong><br>
* Created: <strong>6 Feb 2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.3-alpha
*/
public class WriteProxy {
private final Client client;
private final LocalDB localDB;
private static final Logger logger = EnvoyLog.getLogger(WriteProxy.class);
/**
* Initializes a write proxy using a client and a local database. The
* corresponding cache processors are injected into the caches.
*
* @param client the client used to send messages and message status change
* events
* @param localDB the local database used to cache messages and message status
* change events
* @since Envoy Client v0.3-alpha
*/
public WriteProxy(Client client, LocalDB localDB) {
this.client = client;
this.localDB = localDB;
// Initialize cache processors for messages and message status change events
localDB.getCacheMap().get(Message.class).setProcessor(msg -> {
try {
logger.log(Level.FINER, "Sending cached " + msg);
client.sendMessage(msg);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Could not send cached message: ", e);
}
});
localDB.getCacheMap().get(MessageStatusChange.class).setProcessor(evt -> {
logger.log(Level.FINER, "Sending cached " + evt);
try {
client.sendEvent(evt);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Could not send cached message status change event: ", e);
}
});
}
/**
* Sends cached {@link Message}s and {@link MessageStatusChange}s to the
* server.
*
* @since Envoy Client v0.3-alpha
*/
public void flushCache() {
localDB.getCacheMap().getMap().values().forEach(Cache::relay);
}
/**
* Delivers a message to the server if online. Otherwise the message is cached
* inside the local database.
*
* @param message the message to send
* @throws IOException if the message could not be sent
* @since Envoy Client v0.3-alpha
*/
public void writeMessage(Message message) throws IOException {
if (client.isOnline()) client.sendMessage(message);
else localDB.getCacheMap().getApplicable(Message.class).accept(message);
}
/**
* Delivers a message status change event to the server if online. Otherwise the
* event is cached inside the local database.
*
* @param evt the event to send
* @throws IOException if the event could not be sent
* @since Envoy Client v0.3-alpha
*/
public void writeMessageStatusChange(MessageStatusChange evt) throws IOException {
if (client.isOnline()) client.sendEvent(evt);
else localDB.getCacheMap().getApplicable(MessageStatusChange.class).accept(evt);
}
}

View File

@ -0,0 +1,9 @@
/**
* This package contains all classes related to client-server communication.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
package envoy.client.net;

View 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);
}
}

View File

@ -0,0 +1,169 @@
package envoy.client.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
/**
* This class offers a text field that is automatically equipped with a clear
* button.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ClearableTextField.java</strong><br>
* Created: <strong>25.06.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class ClearableTextField extends GridPane {
private final TextField textField;
private final Button clearButton;
/**
* Constructs a new {@code ClearableTextField} with no initial text and icon
* size 16.
*
* @since Envoy Client v0.1-beta
*/
public ClearableTextField() { this("", 16); }
/**
* Constructs a new {@code ClearableTextField} with initial text and a
* predetermined icon size.
*
* @param text the text that should be displayed by default
* @param size the size of the icon
* @since Envoy Client v0.1-beta
*/
public ClearableTextField(String text, int size) {
// initializing the textField and the button
textField = new TextField(text);
clearButton = new Button("", new ImageView(IconUtil.loadIconThemeSensitive("clear_button", size)));
clearButton.setOnAction(e -> textField.clear());
clearButton.setFocusTraversable(false);
clearButton.getStyleClass().clear();
clearButton.setBackground(Background.EMPTY);
// Adding the two elements to the GridPane
add(textField, 0, 0, 2, 1);
add(clearButton, 1, 0, 1, 1);
// Setting the percent - widths of the two columns.
// Used to locate the button on the right.
final var columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(90);
getColumnConstraints().add(columnConstraints);
final var columnConstraints2 = new ColumnConstraints();
columnConstraints2.setPercentWidth(10);
getColumnConstraints().add(columnConstraints2);
}
/**
* @return the underlying {@code textField}
* @since Envoy Client v0.1-beta
*/
public TextField getTextField() { return textField; }
/**
* This method offers the freedom to perform custom actions when the
* {@code clearButton} has been pressed.
* <p>
* The default is
* <b><code> e -> {clearableTextField.getTextField().clear();}</code></b>
*
* @param onClearButtonAction the action that should be performed
* @since Envoy Client v0.1-beta
*/
public void setClearButtonListener(EventHandler<ActionEvent> onClearButtonAction) { clearButton.setOnAction(onClearButtonAction); }
/**
* @return the current property of the prompt text
* @see javafx.scene.control.TextInputControl#promptTextProperty()
* @since Envoy Client v0.1-beta
*/
public final StringProperty promptTextProperty() { return textField.promptTextProperty(); }
/**
* @return the current prompt text
* @see javafx.scene.control.TextInputControl#getPromptText()
* @since Envoy Client v0.1-beta
*/
public final String getPromptText() { return textField.getPromptText(); }
/**
* @param value the prompt text to display
* @see javafx.scene.control.TextInputControl#setPromptText(java.lang.String)
* @since Envoy Client v0.1-beta
*/
public final void setPromptText(String value) { textField.setPromptText(value); }
/**
* @return the current property of the tooltip
* @see javafx.scene.control.Control#tooltipProperty()
* @since Envoy Client v0.1-beta
*/
public final ObjectProperty<Tooltip> tooltipProperty() { return textField.tooltipProperty(); }
/**
* @param value the new tooltip
* @see javafx.scene.control.Control#setTooltip(javafx.scene.control.Tooltip)
* @since Envoy Client v0.1-beta
*/
public final void setTooltip(Tooltip value) { textField.setTooltip(value); }
/**
* @return the current tooltip
* @see javafx.scene.control.Control#getTooltip()
* @since Envoy Client v0.1-beta
*/
public final Tooltip getTooltip() { return textField.getTooltip(); }
/**
* @return the current property of the context menu
* @see javafx.scene.control.Control#contextMenuProperty()
* @since Envoy Client v0.1-beta
*/
public final ObjectProperty<ContextMenu> contextMenuProperty() { return textField.contextMenuProperty(); }
/**
* @param value the new context menu
* @see javafx.scene.control.Control#setContextMenu(javafx.scene.control.ContextMenu)
* @since Envoy Client v0.1-beta
*/
public final void setContextMenu(ContextMenu value) { textField.setContextMenu(value); }
/**
* @return the current context menu
* @see javafx.scene.control.Control#getContextMenu()
* @since Envoy Client v0.1-beta
*/
public final ContextMenu getContextMenu() { return textField.getContextMenu(); }
/**
* @param value whether this ClearableTextField should be editable
* @see javafx.scene.control.TextInputControl#setEditable(boolean)
* @since Envoy Client v0.1-beta
*/
public final void setEditable(boolean value) { textField.setEditable(value); }
/**
* @return the current property whether this ClearableTextField is editable
* @see javafx.scene.control.TextInputControl#editableProperty()
* @since Envoy Client v0.1-beta
*/
public final BooleanProperty editableProperty() { return textField.editableProperty(); }
/**
* @return whether this {@code ClearableTextField} is editable
* @see javafx.scene.control.TextInputControl#isEditable()
* @since Envoy Client v0.1-beta
*/
public final boolean isEditable() { return textField.isEditable(); }
}

View File

@ -0,0 +1,160 @@
package envoy.client.ui;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.logging.Level;
import javafx.scene.image.Image;
import envoy.client.data.Settings;
import envoy.util.EnvoyLog;
/**
* Provides static utility methods for loading icons from the resource
* folder.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>IconUtil.java</strong><br>
* Created: <strong>16.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class IconUtil {
private IconUtil() {}
/**
* Loads an image from the resource folder.
*
* @param path the path to the icon inside the resource folder
* @return the loaded image
* @since Envoy Client v0.1-beta
*/
public static Image load(String path) {
Image image = null;
try {
image = new Image(IconUtil.class.getResource(path).toExternalForm());
} catch (final NullPointerException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
}
return image;
}
/**
* Loads an image from the resource folder and scales it to the given size.
*
* @param path the path to the icon inside the resource folder
* @param size the size to scale the icon to
* @return the scaled image
* @since Envoy Client v0.1-beta
*/
public static Image load(String path, int size) {
Image image = null;
try {
image = new Image(IconUtil.class.getResource(path).toExternalForm(), size, size, true, true);
} catch (final NullPointerException e) {
EnvoyLog.getLogger(IconUtil.class).log(Level.WARNING, String.format("Could not load image at path %s: ", path), e);
}
return image;
}
/**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the
* resource folder.<br>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the .png suffix
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's load a sample image {@code /icons/abc.png}.<br>
* To do that, we only have to call {@code IconUtil.loadIcon("abc")}
*/
public static Image loadIcon(String name) { return load("/icons/" + name + ".png"); }
/**
* Loads a {@code .png} image from the sub-folder {@code /icons/} of the
* resource folder and scales it to the given size.<br>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the .png suffix
* @param size the size of the image to scale to
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's load a sample image {@code /icons/abc.png} in size 16.<br>
* To do that, we only have to call
* {@code IconUtil.loadIcon("abc", 16)}
*/
public static Image loadIcon(String name, int size) { return load("/icons/" + name + ".png", size); }
/**
* Loads a {@code .png} image whose design depends on the currently active theme
* from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
* resource folder.
* <p>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the "black" or "white" suffix and without
* the .png suffix
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's take two sample images {@code /icons/dark/abc.png} and
* {@code /icons/light/abc.png}, and load one of them.<br>
* To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc")}
*/
public static Image loadIconThemeSensitive(String name) { return loadIcon(themeSpecificSubFolder() + name); }
/**
* Loads a {@code .png} image whose design depends on the currently active theme
* from the sub-folder {@code /icons/dark/} or {@code /icons/light/} of the
* resource folder and scales it to the given size.
* <p>
* The suffix {@code .png} is automatically appended.
*
* @param name the image name without the .png suffix
* @param size the size of the image to scale to
* @return the loaded image
* @since Envoy Client v0.1-beta
* @apiNote let's take two sample images {@code /icons/dark/abc.png} and
* {@code /icons/light/abc.png}, and load one of them in size 16.<br>
* To do that theme sensitive, we only have to call
* {@code IconUtil.loadIconThemeSensitive("abc", 16)}
*/
public static Image loadIconThemeSensitive(String name, int size) { return loadIcon(themeSpecificSubFolder() + name, size); }
/**
*
* Loads images specified by an enum. The images have to be named like the
* lowercase enum constants with {@code .png} extension and be located inside a
* folder with the lowercase name of the enum, which must be contained inside
* the {@code /icons/} folder.
*
* @param <T> the enum that specifies the images to load
* @param enumClass the class of the enum
* @param size the size to scale the images to
* @return a map containing the loaded images with the corresponding enum
* constants as keys
* @since Envoy Client v0.1-beta
*/
public static <T extends Enum<T>> EnumMap<T, Image> loadByEnum(Class<T> enumClass, int size) {
final var icons = new EnumMap<T, Image>(enumClass);
final var path = "/icons/" + enumClass.getSimpleName().toLowerCase() + "/";
for (final var e : EnumSet.allOf(enumClass))
icons.put(e, load(path + e.toString().toLowerCase() + ".png", size));
return icons;
}
/**
* This method should be called if the display of an image depends upon the
* currently active theme.<br>
* In case of a default theme, the string returned will be
* ({@code dark/} or {@code light/}), otherwise it will be empty.
*
* @return the theme specific folder
* @since Envoy Client v0.1-beta
*/
public static String themeSpecificSubFolder() {
return Settings.getInstance().isUsingDefaultTheme() ? Settings.getInstance().getCurrentTheme() + "/" : "";
}
}

View File

@ -0,0 +1,25 @@
package envoy.client.ui;
/**
* This interface defines an action that should be performed when a scene gets
* restored from the scene stack in {@link SceneContext}.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Restorable.java</strong><br>
* Created: <strong>03.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
@FunctionalInterface
public interface Restorable {
/**
* This method is getting called when a scene gets restored.<br>
* Hence, it can contain anything that should be done when the underlying scene
* gets restored.
*
* @since Envoy Client v0.1-beta
*/
void onRestore();
}

View File

@ -0,0 +1,179 @@
package envoy.client.ui;
import java.io.IOException;
import java.util.Stack;
import java.util.logging.Level;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import envoy.client.data.Settings;
import envoy.client.event.ThemeChangeEvent;
import envoy.event.EventBus;
import envoy.util.EnvoyLog;
/**
* Manages a stack of scenes. The most recently added scene is displayed inside
* a stage. When a scene is removed from the stack, its predecessor is
* displayed.
* <p>
* When a scene is loaded, the style sheet for the current theme is applied to
* it.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>SceneContext.java</strong><br>
* Created: <strong>06.06.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class SceneContext {
/**
* Contains information about different scenes and their FXML resource files.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public enum SceneInfo {
/**
* The main scene in which the chat screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
CHAT_SCENE("/fxml/ChatScene.fxml"),
/**
* The scene in which the settings screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
/**
* The scene in which the contact search screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml"),
/**
* The scene in which the group creation screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
GROUP_CREATION_SCENE("/fxml/GroupCreationScene.fxml"),
/**
* The scene in which the login screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
LOGIN_SCENE("/fxml/LoginScene.fxml"),
/**
* The scene in which the info screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
MESSAGE_INFO_SCENE("/fxml/MessageInfoScene.fxml");
/**
* The path to the FXML resource.
*/
public final String path;
SceneInfo(String path) { this.path = path; }
}
private final Stage stage;
private final FXMLLoader loader = new FXMLLoader();
private final Stack<Scene> sceneStack = new Stack<>();
private final Stack<Object> controllerStack = new Stack<>();
private static final Settings settings = Settings.getInstance();
/**
* Initializes the scene context.
*
* @param stage the stage in which scenes will be displayed
* @since Envoy Client v0.1-beta
*/
public SceneContext(Stage stage) {
this.stage = stage;
EventBus.getInstance().register(ThemeChangeEvent.class, theme -> applyCSS());
}
/**
* Loads a new scene specified by a scene info.
*
* @param sceneInfo specifies the scene to load
* @throws RuntimeException if the loading process fails
* @since Envoy Client v0.1-beta
*/
public void load(SceneInfo sceneInfo) {
loader.setRoot(null);
loader.setController(null);
try {
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode);
controllerStack.push(loader.getController());
sceneStack.push(scene);
stage.setScene(scene);
applyCSS();
stage.sizeToScene();
stage.show();
} catch (final IOException e) {
EnvoyLog.getLogger(SceneContext.class).log(Level.SEVERE, String.format("Could not load scene for %s: ", sceneInfo), e);
throw new RuntimeException(e);
}
}
/**
* Removes the current scene and displays the previous one.
*
* @since Envoy Client v0.1-beta
*/
public void pop() {
sceneStack.pop();
controllerStack.pop();
if (!sceneStack.isEmpty()) {
final var newScene = sceneStack.peek();
stage.setScene(newScene);
applyCSS();
stage.sizeToScene();
// If the controller implements the Restorable interface,
// the actions to perform on restoration will be executed here
final var controller = controllerStack.peek();
if (controller instanceof Restorable) ((Restorable) controller).onRestore();
}
stage.show();
}
private void applyCSS() {
if (!sceneStack.isEmpty()) {
final var styleSheets = stage.getScene().getStylesheets();
final var themeCSS = "/css/" + settings.getCurrentTheme() + ".css";
styleSheets.clear();
styleSheets.addAll(getClass().getResource("/css/base.css").toExternalForm(), getClass().getResource(themeCSS).toExternalForm());
}
}
/**
* @param <T> the type of the controller
* @return the controller used by the current scene
* @since Envoy Client v0.1-beta
*/
public <T> T getController() { return (T) controllerStack.peek(); }
/**
* @return the stage in which the scenes are displayed
* @since Envoy Client v0.1-beta
*/
public Stage getStage() { return stage; }
}

View File

@ -0,0 +1,135 @@
package envoy.client.ui;
import java.io.File;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene;
import envoy.data.GroupMessage;
import envoy.data.Message;
import envoy.event.GroupMessageStatusChange;
import envoy.event.MessageStatusChange;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
/**
* Handles application startup and shutdown.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Startup.java</strong><br>
* Created: <strong>26.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public final class Startup extends Application {
/**
* The version of this client. Used to verify compatibility with the server.
*
* @since Envoy Client v0.1-beta
*/
public static final String VERSION = "0.1-beta";
private LocalDB localDB;
private Client client;
private static final ClientConfig config = ClientConfig.getInstance();
private static final Logger logger = EnvoyLog.getLogger(Startup.class);
/**
* Loads the configuration, initializes the client and the local database and
* delegates the rest of the startup process to {@link LoginScene}.
*
* @since Envoy Client v0.1-beta
*/
@Override
public void start(Stage stage) throws Exception {
try {
// Load the configuration from client.properties first
final Properties properties = new Properties();
properties.load(Startup.class.getClassLoader().getResourceAsStream("client.properties"));
config.load(properties);
// Override configuration values with command line arguments
final String[] args = getParameters().getRaw().toArray(new String[0]);
if (args.length > 0) config.load(args);
// Check if all mandatory configuration values have been initialized
if (!config.isInitialized()) throw new EnvoyException("Configuration is not fully initialized");
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e);
logger.log(Level.SEVERE, "Error loading configuration values: ", e);
e.printStackTrace();
System.exit(1);
}
// Setup logger for the envoy package
EnvoyLog.initialize(config);
EnvoyLog.attach("envoy");
EnvoyLog.setFileLevelBarrier(config.getFileLevelBarrier());
EnvoyLog.setConsoleLevelBarrier(config.getConsoleLevelBarrier());
logger.log(Level.INFO, "Envoy starting...");
// Initialize the local database
if (config.isIgnoreLocalDB()) {
localDB = new TransientLocalDB();
new Alert(AlertType.WARNING, "Ignoring local database.\nMessages will not be saved!").showAndWait();
} else try {
localDB = new PersistentLocalDB(new File(config.getHomeDirectory(), config.getLocalDB().getPath()));
} catch (final IOException e3) {
logger.log(Level.SEVERE, "Could not initialize local database: ", e3);
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e3).showAndWait();
System.exit(1);
return;
}
// Initialize client and unread message cache
client = new Client();
final var cacheMap = new CacheMap();
cacheMap.put(Message.class, new Cache<Message>());
cacheMap.put(GroupMessage.class, new Cache<GroupMessage>());
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
stage.setTitle("Envoy");
stage.getIcons().add(IconUtil.loadIcon("envoy_logo"));
final var sceneContext = new SceneContext(stage);
sceneContext.load(SceneInfo.LOGIN_SCENE);
sceneContext.<LoginScene>getController().initializeData(client, localDB, cacheMap, sceneContext);
}
/**
* Closes the client connection and saves the local database and settings.
*
* @since Envoy Client v0.1-beta
*/
@Override
public void stop() {
try {
logger.log(Level.INFO, "Closing connection...");
client.close();
logger.log(Level.INFO, "Saving local database and settings...");
localDB.save();
Settings.getInstance().save();
logger.log(Level.INFO, "Envoy was terminated by its user");
} catch (final Exception e) {
logger.log(Level.SEVERE, "Unable to save local files: ", e);
}
}
}

View File

@ -0,0 +1,100 @@
package envoy.client.ui;
import java.awt.*;
import java.awt.TrayIcon.MessageType;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.logging.Level;
import envoy.client.event.MessageCreationEvent;
import envoy.data.Message;
import envoy.event.EventBus;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>StatusTrayIcon.java</strong><br>
* Created: <strong>3 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.2-alpha
*/
public class StatusTrayIcon {
/**
* The {@link TrayIcon} provided by the System Tray API for controlling the
* system tray. This includes displaying the icon, but also creating
* notifications when new messages are received.
*/
private final TrayIcon trayIcon;
/**
* A received {@link Message} is only displayed as a system tray notification if
* this variable is set to {@code true}.
*/
private boolean displayMessages = false;
/**
* Creates a {@link StatusTrayIcon} with the Envoy logo, a tool tip and a pop-up
* menu.
*
* @param focusTarget the {@link Window} which focus determines if message
* notifications are displayed
* @throws EnvoyException if the currently used OS does not support the System
* Tray API
* @since Envoy Client v0.2-alpha
*/
public StatusTrayIcon(Window focusTarget) throws EnvoyException {
if (!SystemTray.isSupported()) throw new EnvoyException("The Envoy tray icon is not supported.");
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
final Image img = Toolkit.getDefaultToolkit().createImage(loader.getResource("envoy_logo.png"));
trayIcon = new TrayIcon(img, "Envoy Client");
trayIcon.setImageAutoSize(true);
trayIcon.setToolTip("You are notified if you have unread messages.");
final PopupMenu popup = new PopupMenu();
final MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.addActionListener(evt -> System.exit(0));
popup.add(exitMenuItem);
trayIcon.setPopupMenu(popup);
// Only display messages if the chat window is not focused
focusTarget.addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) { displayMessages = false; }
@Override
public void windowLostFocus(WindowEvent e) { displayMessages = true; }
});
// Show the window if the user clicks on the icon
trayIcon.addActionListener(evt -> { focusTarget.setVisible(true); focusTarget.requestFocus(); });
// Start processing message events
// TODO: Handle other message types
EventBus.getInstance()
.register(MessageCreationEvent.class,
evt -> { if (displayMessages) trayIcon.displayMessage("New message received", evt.get().getText(), MessageType.INFO); });
}
/**
* Makes this {@link StatusTrayIcon} appear in the system tray.
*
* @throws EnvoyException if the status icon could not be attaches to the system
* tray for system-internal reasons
* @since Envoy Client v0.2-alpha
*/
public void show() throws EnvoyException {
try {
SystemTray.getSystemTray().add(trayIcon);
} catch (final AWTException e) {
EnvoyLog.getLogger(StatusTrayIcon.class).log(Level.INFO, "Could not display StatusTrayIcon: ", e);
throw new EnvoyException("Could not attach Envoy tray icon to system tray.", e);
}
}
}

View File

@ -0,0 +1,570 @@
package envoy.client.ui.controller;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.animation.RotateTransition;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.util.Duration;
import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder;
import envoy.client.event.MessageCreationEvent;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
import envoy.client.ui.IconUtil;
import envoy.client.ui.Restorable;
import envoy.client.ui.SceneContext;
import envoy.client.ui.listcell.ContactListCellFactory;
import envoy.client.ui.listcell.MessageControl;
import envoy.client.ui.listcell.MessageListCellFactory;
import envoy.data.*;
import envoy.data.Attachment.AttachmentType;
import envoy.event.*;
import envoy.event.contact.ContactOperation;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ChatSceneController.java</strong><br>
* Created: <strong>26.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class ChatScene implements Restorable {
@FXML
private GridPane scene;
@FXML
private Label contactLabel;
@FXML
private ListView<Message> messageList;
@FXML
private ListView<Chat> chatList;
@FXML
private Button postButton;
@FXML
private Button voiceButton;
@FXML
private Button attachmentButton;
@FXML
private Button settingsButton;
@FXML
private Button rotateButton;
@FXML
private TextArea messageTextArea;
@FXML
private Label remainingChars;
@FXML
private Label infoLabel;
@FXML
private MenuItem deleteContactMenuItem;
@FXML
private ImageView attachmentView;
private LocalDB localDB;
private Client client;
private WriteProxy writeProxy;
private SceneContext sceneContext;
private Chat currentChat;
private AudioRecorder recorder;
private boolean recording;
private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled;
private static final Settings settings = Settings.getInstance();
private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
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.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void initialize() {
// Initialize message and user rendering
messageList.setCellFactory(MessageListCellFactory::new);
chatList.setCellFactory(ContactListCellFactory::new);
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
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);
rotateButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("rotate", (int) (DEFAULT_ICON_SIZE * 1.5))));
// Listen to received messages
eventBus.register(MessageCreationEvent.class, e -> {
final var message = e.get();
localDB.getChat(message instanceof GroupMessage ? message.getRecipientID() : message.getSenderID()).ifPresent(chat -> {
chat.insert(message);
if (chat.equals(currentChat)) {
try {
currentChat.read(writeProxy);
} catch (final IOException e1) {
logger.log(Level.WARNING, "Could not read current chat: ", e1);
}
Platform.runLater(() -> { messageList.refresh(); scrollToMessageListEnd(); });
} else chat.incrementUnreadAmount();
// Moving chat with most recent unreadMessages to the top
Platform.runLater(() -> {
chatList.getItems().remove(chat);
chatList.getItems().add(0, chat);
if (chat.equals(currentChat)) chatList.getSelectionModel().select(0);
localDB.getChats().remove(chat);
localDB.getChats().add(0, chat);
});
});
});
// Listen to message status changes
eventBus.register(MessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(message -> {
message.setStatus(e.get());
// Update UI if in current chat
if (currentChat != null && message.getSenderID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
}));
eventBus.register(GroupMessageStatusChange.class, e -> localDB.getMessage(e.getID()).ifPresent(groupMessage -> {
((GroupMessage) groupMessage).getMemberStatuses().replace(e.getMemberID(), e.get());
// Update UI if in current chat
if (currentChat != null && groupMessage.getRecipientID() == currentChat.getRecipient().getID()) Platform.runLater(messageList::refresh);
}));
// Listen to user status changes
eventBus.register(UserStatusChange.class,
e -> chatList.getItems()
.stream()
.filter(c -> c.getRecipient().getID() == e.getID())
.findAny()
.map(Chat::getRecipient)
.ifPresent(u -> { ((User) u).setStatus(e.get()); Platform.runLater(chatList::refresh); }));
// Listen to contacts changes
eventBus.register(ContactOperation.class, e -> {
final var contact = e.get();
switch (e.getOperationType()) {
case ADD:
localDB.getUsers().put(contact.getName(), contact);
Chat chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
localDB.getChats().add(chat);
Platform.runLater(() -> chatList.getItems().add(chat));
break;
case REMOVE:
localDB.getUsers().remove(contact.getName());
localDB.getChats().removeIf(c -> c.getRecipient().getID() == contact.getID());
Platform.runLater(() -> chatList.getItems().removeIf(c -> c.getRecipient().getID() == contact.getID()));
break;
}
});
}
/**
* Initializes all necessary data via dependency injection-
*
* @param sceneContext the scene context used to load other scenes
* @param localDB the local database form which chats and users are loaded
* @param client the client used to request ID generators
* @param writeProxy the write proxy used to send messages and other data to
* the server
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext, LocalDB localDB, Client client, WriteProxy writeProxy) {
this.sceneContext = sceneContext;
this.localDB = localDB;
this.client = client;
this.writeProxy = writeProxy;
chatList.setItems(FXCollections.observableList(localDB.getChats()));
contactLabel.setText(localDB.getUser().getName());
MessageControl.setUser(localDB.getUser());
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
recorder = new AudioRecorder();
}
@Override
public void onRestore() { updateRemainingCharsLabel(); }
/**
* Actions to perform when the list of contacts has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void chatListClicked() {
final Contact user = chatList.getSelectionModel().getSelectedItem().getRecipient();
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
// Load the chat
currentChat = localDB.getChat(user.getID()).get();
messageList.setItems(FXCollections.observableList(currentChat.getMessages()));
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount() - 1;
messageList.scrollTo(scrollIndex);
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
deleteContactMenuItem.setText("Delete " + user.getName());
// Read the current chat
try {
currentChat.read(writeProxy);
} catch (final IOException e) {
logger.log(Level.WARNING, "Could not read current chat.", e);
}
// Discard the pending attachment
if (recorder.isRecording()) {
recorder.cancel();
recording = false;
}
pendingAttachment = null;
updateAttachmentView(false);
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());
attachmentButton.setDisable(false);
chatList.refresh();
}
/**
* Actions to perform when the Settings Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void settingsButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
sceneContext.<SettingsScene>getController().initializeData(sceneContext);
}
/**
* Actions to perform when the "Add Contact" - Button has been clicked.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void addContactButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext, localDB);
}
@FXML
private void voiceButtonClicked() {
new Thread(() -> {
try {
if (!recording) {
recording = true;
Platform.runLater(() -> {
voiceButton.setText("Recording");
voiceButton.setGraphic(new ImageView(IconUtil.loadIcon("microphone_recording", DEFAULT_ICON_SIZE)));
});
recorder.start();
} else {
pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE);
recording = false;
Platform.runLater(() -> {
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
voiceButton.setText(null);
checkPostConditions(false);
updateAttachmentView(true);
});
}
} catch (final EnvoyException e) {
logger.log(Level.SEVERE, "Could not record audio: ", e);
Platform.runLater(new Alert(AlertType.ERROR, "Could not record audio")::showAndWait);
}
}).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();
}
}
}
/**
* Rotates every element in our application by 360° in at most 2.75s.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void doABarrelRoll() {
// contains all Node objects in ChatScene in alphabetical order
final var rotatableNodes = new Node[] { attachmentButton, attachmentView, contactLabel, infoLabel, messageList, messageTextArea,
postButton, remainingChars, rotateButton, scene, settingsButton, userList, voiceButton };
final var random = new Random();
for (final var node : rotatableNodes) {
// Defines at most four whole rotation in at most 4s
final var rotateTransition = new RotateTransition(Duration.seconds(random.nextDouble() * 3 + 1), node);
rotateTransition.setByAngle((random.nextInt(3) + 1) * 360);
rotateTransition.play();
// This is needed as for some strange reason objects could stop before being
// rotated back to 0°
rotateTransition.setOnFinished(e -> node.setRotate(0));
}
}
/**
* Checks the text length of the {@code messageTextArea}, adjusts the
* {@code remainingChars} label and checks whether to send the message
* automatically.
*
* @param e the key event that will be analyzed for a post request
* @since Envoy Client v0.1-beta
*/
@FXML
private void checkKeyCombination(KeyEvent e) {
// Checks whether the text is too long
messageTextUpdated();
// Automatic sending of messages via (ctrl +) enter
checkPostConditions(e);
}
/**
* @param e the keys that have been pressed
* @since Envoy Client v0.1-beta
*/
@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() && 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))
// Informing the user that he is a f*cking moron and should use Envoy online
// because he ran out of messageIDs to use
updateInfoLabel(noMoreMessaging, "infoLabel-error");
}
}
/**
* Actions to perform when the text was updated in the messageTextArea.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void messageTextUpdated() {
// Truncating messages that are too long and staying at the same position
if (messageTextArea.getText().length() >= MAX_MESSAGE_LENGTH) {
messageTextArea.setText(messageTextArea.getText().substring(0, MAX_MESSAGE_LENGTH));
messageTextArea.positionCaret(MAX_MESSAGE_LENGTH);
messageTextArea.setScrollTop(Double.MAX_VALUE);
}
updateRemainingCharsLabel();
}
/**
* Sets the text and text color of the {@code remainingChars} label.
*
* @since Envoy Client v0.1-beta
*/
private void updateRemainingCharsLabel() {
final int currentLength = messageTextArea.getText().length();
final int remainingLength = MAX_MESSAGE_LENGTH - currentLength;
remainingChars.setText(String.format("remaining chars: %d/%d", remainingLength, MAX_MESSAGE_LENGTH));
remainingChars.setTextFill(Color.rgb(currentLength, remainingLength, 0, 1));
}
/**
* Sends a new {@link Message} or {@link GroupMessage} to the server based on
* the text entered in the {@code messageTextArea} and the given attachment.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void postMessage() {
postingPermanentlyDisabled = !(client.isOnline() || localDB.getIDGenerator().hasNext());
if (postingPermanentlyDisabled) {
postButton.setDisable(true);
messageTextArea.setDisable(true);
messageTextArea.clear();
updateInfoLabel("You need to go online to send more messages", "infoLabel-error");
return;
}
final var text = messageTextArea.getText().strip();
try {
// Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text);
// Setting an attachment, if present
if (pendingAttachment != null) {
builder.setAttachment(pendingAttachment);
pendingAttachment = null;
updateAttachmentView(false);
}
// Building the final message
final var message = currentChat.getRecipient() instanceof Group ? builder.buildGroupMessage((Group) currentChat.getRecipient())
: builder.build();
// Send message
writeProxy.writeMessage(message);
// Add message to LocalDB and update UI
currentChat.insert(message);
// Moving currentChat to the top
Platform.runLater(() -> {
chatList.getItems().remove(currentChat);
chatList.getItems().add(0, currentChat);
chatList.getSelectionModel().select(0);
localDB.getChats().remove(currentChat);
localDB.getChats().add(0, currentChat);
});
messageList.refresh();
scrollToMessageListEnd();
// Request a new ID generator if all IDs were used
if (!localDB.getIDGenerator().hasNext() && client.isOnline()) client.requestIdGenerator();
} catch (final IOException e) {
logger.log(Level.SEVERE, "Error while sending message: ", e);
new Alert(AlertType.ERROR, "An error occured while sending the message!").showAndWait();
}
// Clear text field and disable post button
messageTextArea.setText("");
postButton.setDisable(true);
updateRemainingCharsLabel();
}
/**
* Scrolls to the bottom of the {@code messageList}.
*
* @since Envoy Client v0.1-beta
*/
private void scrollToMessageListEnd() { messageList.scrollTo(messageList.getItems().size() - 1); }
/**
* Updates the {@code infoLabel}.
*
* @param text the text to use
* @param infoLabelID the id the the {@code infoLabel} should have so that it
* can be styled accordingly in CSS
* @since Envoy Client v0.1-beta
*/
private void updateInfoLabel(String text, String infoLabelID) {
infoLabel.setText(text);
infoLabel.setId(infoLabelID);
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
@FXML
private void deleteContact() { try {} catch (final NullPointerException e) {} }
@FXML
private void copyAndPostMessage() {
final var messageText = messageTextArea.getText();
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(messageText), null);
postMessage();
messageTextArea.setText(messageText);
updateRemainingCharsLabel();
postButton.setDisable(messageText.isBlank());
}
}

View File

@ -0,0 +1,130 @@
package envoy.client.ui.controller;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ListView;
import envoy.client.data.Chat;
import envoy.client.data.LocalDB;
import envoy.client.event.SendEvent;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.listcell.ContactListCellFactory;
import envoy.event.ElementOperation;
import envoy.event.EventBus;
import envoy.event.contact.ContactOperation;
import envoy.event.contact.ContactSearchRequest;
import envoy.event.contact.ContactSearchResult;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ContactSearchSceneController.java</strong><br>
* Created: <strong>07.06.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class ContactSearchScene {
@FXML
private ClearableTextField searchBar;
@FXML
private ListView<Chat> chatList;
private SceneContext sceneContext;
private LocalDB localDB;
private static EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
/**
* @param sceneContext enables the user to return to the chat scene
* @param localDB the local database to which new contacts are added
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
this.sceneContext = sceneContext;
this.localDB = localDB;
}
@FXML
private void initialize() {
chatList.setCellFactory(ContactListCellFactory::new);
searchBar.setClearButtonListener(e -> { searchBar.getTextField().clear(); chatList.getItems().clear(); });
eventBus.register(ContactSearchResult.class,
response -> Platform.runLater(() -> {
chatList.getItems().clear();
chatList.getItems().addAll(response.get().stream().map(Chat::new).collect(Collectors.toList()));
}));
}
/**
* Disables the clear and search button if no text is present in the search bar.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void sendRequest() {
final var text = searchBar.getTextField().getText().strip();
if (!text.isBlank()) eventBus.dispatch(new SendEvent(new ContactSearchRequest(text)));
else chatList.getItems().clear();
}
/**
* Clears the text in the search bar and the items shown in the list.
* Additionally disables both clear and search button.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void clear() {
searchBar.getTextField().setText(null);
chatList.getItems().clear();
}
/**
* Sends an {@link ContactOperation} for every selected contact to the
* server.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void chatListClicked() {
final var chat = chatList.getSelectionModel().getSelectedItem();
if (chat != null) {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Add Contact to Contact List");
alert.setHeaderText("Add the user " + chat.getRecipient().getName() + " to your contact list?");
// Normally, this would be total BS (we are already on the FX Thread), however
// it could be proven that the creation of this dialog wrapped in
// Platform.runLater is less error-prone than without it
Platform.runLater(() -> alert.showAndWait().filter(btn -> btn == ButtonType.OK).ifPresent(btn -> {
final var event = new ContactOperation(chat.getRecipient(), ElementOperation.ADD);
// Sends the event to the server
eventBus.dispatch(new SendEvent(event));
// Updates the UI
eventBus.dispatch(event);
logger.log(Level.INFO, "Added contact " + chat.getRecipient());
}));
}
}
@FXML
private void newGroupButtonClicked() {
sceneContext.load(SceneContext.SceneInfo.GROUP_CREATION_SCENE);
sceneContext.<GroupCreationScene>getController().initializeData(sceneContext, localDB);
}
@FXML
private void backButtonClicked() { sceneContext.pop(); }
}

View File

@ -0,0 +1,109 @@
package envoy.client.ui.controller;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.Chat;
import envoy.client.data.LocalDB;
import envoy.client.event.SendEvent;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.listcell.ContactListCellFactory;
import envoy.data.Group;
import envoy.event.EventBus;
import envoy.event.GroupCreation;
import envoy.util.Bounds;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>ContactSearchSceneController.java</strong><br>
* Created: <strong>07.06.2020</strong><br>
*
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public class GroupCreationScene {
@FXML
private Button createButton;
@FXML
private ClearableTextField groupNameField;
@FXML
private ListView<Chat> chatList;
private SceneContext sceneContext;
private static final EventBus eventBus = EventBus.getInstance();
@FXML
private void initialize() {
chatList.setCellFactory(ContactListCellFactory::new);
chatList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
groupNameField.setClearButtonListener(e -> { groupNameField.getTextField().clear(); createButton.setDisable(true); });
}
/**
* @param sceneContext enables the user to return to the chat scene
* @param localDB the local database from which potential group members can
* be selected
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext, LocalDB localDB) {
this.sceneContext = sceneContext;
Platform.runLater(() -> chatList.getItems()
.addAll(localDB.getChats()
.stream()
.filter(c -> !(c.getRecipient() instanceof Group))
.filter(c -> c.getRecipient().getID() != localDB.getUser().getID())
.collect(Collectors.toList())));
}
/**
* Enables the {@code createButton} if at least one contact is selected.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void chatListClicked() {
createButton.setDisable(chatList.getSelectionModel().isEmpty() || groupNameField.getTextField().getText().isBlank());
}
/**
* Checks, whether the {@code createButton} can be enabled because text is
* present in the textfield.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void textUpdated() { createButton.setDisable(groupNameField.getTextField().getText().isBlank()); }
/**
* Sends a {@link GroupCreation} to the server and closes this scene.
* <p>
* If the given group name is not valid, an error is displayed instead.
*
* @since Envoy Client v0.1-beta
*/
@FXML
private void createButtonClicked() {
final var name = groupNameField.getTextField().getText();
if (!Bounds.isValidContactName(name)) {
new Alert(AlertType.ERROR, "The entered group name is not valid (" + Bounds.CONTACT_NAME_PATTERN + ")").showAndWait();
groupNameField.getTextField().clear();
} else {
eventBus.dispatch(new SendEvent(new GroupCreation(name,
chatList.getSelectionModel().getSelectedItems().stream().map(c -> c.getRecipient().getID()).collect(Collectors.toSet()))));
new Alert(AlertType.INFORMATION, String.format("Group '%s' successfully created.", name)).showAndWait();
sceneContext.pop();
}
}
@FXML
private void backButtonClicked() { sceneContext.pop(); }
}

View File

@ -0,0 +1,199 @@
package envoy.client.ui.controller;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
import envoy.client.ui.ClearableTextField;
import envoy.client.ui.SceneContext;
import envoy.client.ui.Startup;
import envoy.data.LoginCredentials;
import envoy.data.User;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
import envoy.event.HandshakeRejection;
import envoy.exception.EnvoyException;
import envoy.util.Bounds;
import envoy.util.EnvoyLog;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>LoginDialog.java</strong><br>
* Created: <strong>03.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
public final class LoginScene {
@FXML
private ClearableTextField userTextField;
@FXML
private PasswordField passwordField;
@FXML
private PasswordField repeatPasswordField;
@FXML
private Label repeatPasswordLabel;
@FXML
private CheckBox registerCheckBox;
@FXML
private Label connectionLabel;
private Client client;
private LocalDB localDB;
private CacheMap cacheMap;
private SceneContext sceneContext;
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
private static final EventBus eventBus = EventBus.getInstance();
private static final ClientConfig config = ClientConfig.getInstance();
@FXML
private void initialize() {
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
// Show an alert after an unsuccessful handshake
eventBus.register(HandshakeRejection.class, e -> Platform.runLater(() -> { new Alert(AlertType.ERROR, e.get()).showAndWait(); }));
}
/**
* Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
*
* @param client the client used to perform the handshake
* @param localDB the local database used for offline login
* @param cacheMap the map of all caches needed
* @param sceneContext the scene context used to initialize the chat scene
* @since Envoy Client v0.1-beta
*/
public void initializeData(Client client, LocalDB localDB, CacheMap cacheMap, SceneContext sceneContext) {
this.client = client;
this.localDB = localDB;
this.cacheMap = cacheMap;
this.sceneContext = sceneContext;
// Prepare handshake
localDB.loadIDGenerator();
// Set initial cursor
userTextField.requestFocus();
// Perform automatic login if configured
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
}
@FXML
private void loginButtonPressed() {
// Prevent registration with unequal passwords
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
repeatPasswordField.clear();
} else if (!Bounds.isValidContactName(userTextField.getTextField().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));
}
@FXML
private void offlineModeButtonPressed() {
attemptOfflineMode(new LoginCredentials(userTextField.getTextField().getText(), passwordField.getText(), false, Startup.VERSION));
}
@FXML
private void registerCheckboxChanged() {
// Make repeat password field and label visible / invisible
repeatPasswordField.setVisible(registerCheckBox.isSelected());
repeatPasswordLabel.setVisible(registerCheckBox.isSelected());
}
@FXML
private void abortLogin() {
logger.log(Level.INFO, "The login process has been cancelled. Exiting...");
System.exit(0);
}
private void performHandshake(LoginCredentials credentials) {
try {
client.performHandshake(credentials, cacheMap);
if (client.isOnline()) {
loadChatScene();
client.initReceiver(localDB, cacheMap);
}
} catch (IOException | InterruptedException | TimeoutException e) {
logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
attemptOfflineMode(credentials);
}
}
private void attemptOfflineMode(LoginCredentials credentials) {
try {
// Try entering offline mode
localDB.loadUsers();
final User clientUser = (User) localDB.getUsers().get(credentials.getIdentifier());
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser);
loadChatScene();
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e);
System.exit(1);
}
}
private void loadChatScene() {
// Set client user in local database
localDB.setUser(client.getSender());
// Initialize chats in local database
try {
localDB.initializeUserStorage();
localDB.loadUserData();
} catch (final FileNotFoundException e) {
// The local database file has not yet been created, probably first login
} catch (final Exception e) {
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
logger.log(Level.WARNING, "Could not load local database: ", e);
}
// Initialize write proxy
final var writeProxy = new WriteProxy(client, localDB);
localDB.synchronize();
if (client.isOnline()) writeProxy.flushCache();
else
// Set all contacts to offline mode
localDB.getChats()
.stream()
.map(Chat::getRecipient)
.filter(User.class::isInstance)
.map(User.class::cast)
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
// Load ChatScene
sceneContext.pop();
sceneContext.getStage().setMinHeight(400);
sceneContext.getStage().setMinWidth(350);
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
}
}

View File

@ -0,0 +1,59 @@
package envoy.client.ui.controller;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import envoy.client.ui.SceneContext;
import envoy.client.ui.settings.GeneralSettingsPane;
import envoy.client.ui.settings.SettingsPane;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsSceneController.java</strong><br>
* Created: <strong>10.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class SettingsScene {
@FXML
private ListView<SettingsPane> settingsList;
@FXML
private TitledPane titledPane;
private SceneContext sceneContext;
/**
* @param sceneContext enables the user to return to the chat scene
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext) { this.sceneContext = sceneContext; }
@FXML
private void initialize() {
settingsList.setCellFactory(listView -> new ListCell<>() {
@Override
protected void updateItem(SettingsPane item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) setGraphic(new Label(item.getTitle()));
}
});
settingsList.getItems().add(new GeneralSettingsPane());
}
@FXML
private void settingsListClicked() {
final var pane = settingsList.getSelectionModel().getSelectedItem();
if (pane != null) {
titledPane.setText(pane.getTitle());
titledPane.setContent(pane);
}
}
@FXML
private void backButtonClicked() { sceneContext.pop(); }
}

View File

@ -0,0 +1,11 @@
/**
* Contains JavaFX scene controllers.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>08.06.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
package envoy.client.ui.controller;

View File

@ -0,0 +1,58 @@
package envoy.client.ui.listcell;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import envoy.client.data.Chat;
import envoy.data.Contact;
import envoy.data.Group;
import envoy.data.User;
/**
* This class formats a single {@link Contact} into a UI component.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ContactControl.java</strong><br>
* Created: <strong>01.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class ChatControl extends HBox {
/**
* @param chat the chat to display
* @since Envoy Client v0.1-beta
*/
public ChatControl(Chat chat) {
// Container with contact name
final var vBox = new VBox();
final var nameLabel = new Label(chat.getRecipient().getName());
nameLabel.setWrapText(true);
vBox.getChildren().add(nameLabel);
if (chat.getRecipient() instanceof User) {
// Online status
final var user = (User) chat.getRecipient();
final var statusLabel = new Label(user.getStatus().toString());
statusLabel.getStyleClass().add(user.getStatus().toString().toLowerCase());
vBox.getChildren().add(statusLabel);
} else // Member count
vBox.getChildren().add(new Label(((Group) chat.getRecipient()).getContacts().size() + " members"));
getChildren().add(vBox);
if (chat.getUnreadAmount() != 0) {
Region spacing = new Region();
setHgrow(spacing, Priority.ALWAYS);
getChildren().add(spacing);
final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount()));
unreadMessagesLabel.setMinSize(15, 15);
var vBox2 = new VBox();
vBox2.setAlignment(Pos.CENTER_RIGHT);
unreadMessagesLabel.setAlignment(Pos.CENTER);
unreadMessagesLabel.getStyleClass().add("unreadMessagesAmount");
vBox2.getChildren().add(unreadMessagesLabel);
getChildren().add(vBox2);
}
}
}

View File

@ -0,0 +1,44 @@
package envoy.client.ui.listcell;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import envoy.client.data.Chat;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>UserListCell.java</strong><br>
* Created: <strong>28.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class ContactListCellFactory extends ListCell<Chat> {
private final ListView<Chat> listView;
/**
* @param listView the list view inside which this cell is contained
* @since Envoy Client v0.1-beta
*/
public ContactListCellFactory(ListView<Chat> listView) { this.listView = listView; }
/**
* Displays the name of a contact. If the contact is a user, their online status
* is displayed as well.
*
* @since Envoy Client v0.1-beta
*/
@Override
protected void updateItem(Chat chat, boolean empty) {
super.updateItem(chat, empty);
if (empty || chat.getRecipient() == null) {
setText(null);
setGraphic(null);
} else {
final var control = new ChatControl(chat);
prefWidthProperty().bind(listView.widthProperty().subtract(40));
setGraphic(control);
}
}
}

View File

@ -0,0 +1,123 @@
package envoy.client.ui.listcell;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.io.ByteArrayInputStream;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.geometry.Insets;
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 envoy.client.ui.AudioControl;
import envoy.client.ui.IconUtil;
import envoy.data.Message;
import envoy.data.Message.MessageStatus;
import envoy.data.User;
import envoy.util.EnvoyLog;
/**
* This class formats a single {@link Message} into a UI component.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageControl.java</strong><br>
* Created: <strong>01.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
public class MessageControl extends Label {
private static User client;
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
private static final Map<MessageStatus, Image> statusImages = IconUtil.loadByEnum(MessageStatus.class, 16);
private static final Logger logger = EnvoyLog.getLogger(MessageControl.class);
/**
*
* @param message the message that should be formatted
* @since Envoy Client v0.1-beta
*/
public MessageControl(Message message) {
// Creating the underlying VBox and the dateLabel
final var vbox = new VBox(new Label(dateFormat.format(message.getCreationDate())));
// 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");
copyMenuItem.setOnAction(e -> copyMessage(message));
deleteMenuItem.setOnAction(e -> deleteMessage(message));
forwardMenuItem.setOnAction(e -> forwardMessage(message));
quoteMenuItem.setOnAction(e -> quoteMessage(message));
infoMenuItem.setOnAction(e -> loadMessageInfoScene(message));
contextMenu.getItems().addAll(copyMenuItem, deleteMenuItem, forwardMenuItem, quoteMenuItem, infoMenuItem);
// Handling message attachment display
if (message.hasAttachment()) {
switch (message.getAttachment().getType()) {
case PICTURE:
vbox.getChildren().add(new ImageView(new Image(new ByteArrayInputStream(message.getAttachment().getData()), 256, 256, true, true)));
break;
case VIDEO:
break;
case VOICE:
vbox.getChildren().add(new AudioControl(message.getAttachment().getData()));
break;
case DOCUMENT:
break;
}
final var saveAttachment = new MenuItem("Save attachment");
saveAttachment.setOnAction(e -> saveAttachment(message));
contextMenu.getItems().add(saveAttachment);
}
// Creating the textLabel
final var textLabel = new Label(message.getText());
textLabel.setWrapText(true);
vbox.getChildren().add(textLabel);
// Setting the message status icon and background color
if (message.getSenderID() == client.getID()) {
final var statusIcon = new ImageView(statusImages.get(message.getStatus()));
statusIcon.setPreserveRatio(true);
vbox.getChildren().add(statusIcon);
getStyleClass().add("own-message");
} else getStyleClass().add("received-message");
// Adjusting height and weight of the cell to the corresponding ListView
paddingProperty().setValue(new Insets(5, 20, 5, 20));
setContextMenu(contextMenu);
setGraphic(vbox);
}
// Context Menu actions
private void copyMessage(Message message) {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(message.getText()), null);
}
private void deleteMessage(Message message) { logger.log(Level.FINEST, "message deletion was requested for " + message); }
private void forwardMessage(Message message) { logger.log(Level.FINEST, "message forwarding was requested for " + message); }
private void quoteMessage(Message message) { logger.log(Level.FINEST, "message quotation was requested for " + message); }
private void loadMessageInfoScene(Message message) { logger.log(Level.FINEST, "message info scene was requested for " + message); }
private void saveAttachment(Message message) { logger.log(Level.FINEST, "attachment saving was requested for " + message); }
/**
* @param client the user who has logged in
* @since Envoy Client v0.1-beta
*/
public static void setUser(User client) { MessageControl.client = client; }
}

View File

@ -0,0 +1,52 @@
package envoy.client.ui.listcell;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Tooltip;
import javafx.stage.PopupWindow.AnchorLocation;
import envoy.data.Message;
/**
* Displays a single message inside the message list.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>MessageListCellFactory.java</strong><br>
* Created: <strong>28.03.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class MessageListCellFactory extends ListCell<Message> {
private final ListView<Message> listView;
/**
* @param listView the list view inside which this cell is contained
* @since Envoy Client v0.1-beta
*/
public MessageListCellFactory(ListView<Message> listView) { this.listView = listView; }
/**
* Displays the text, the data of creation and the status of a message.
*
* @since Envoy v0.1-beta
*/
@Override
protected void updateItem(Message message, boolean empty) {
super.updateItem(message, empty);
if (empty || message == null) {
setText(null);
setGraphic(null);
} else {
final var control = new MessageControl(message);
control.prefWidthProperty().bind(listView.widthProperty().subtract(40));
// Creating the Tooltip to deselect a message
final var tooltip = new Tooltip("You can select a message by clicking on it \nand deselect it by pressing \"ctrl\" and clicking on it");
tooltip.setWrapText(true);
tooltip.setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
setTooltip(tooltip);
setGraphic(control);
}
}
}

View File

@ -0,0 +1,12 @@
/**
* This package contains custom list cells that are used to display certain
* things.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>30.06.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.1-beta
*/
package envoy.client.ui.listcell;

View File

@ -0,0 +1,9 @@
/**
* This package contains classes defining the user interface.
*
* @author Leon Hofmeister
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
package envoy.client.ui;

View File

@ -0,0 +1,57 @@
package envoy.client.ui.settings;
import java.util.List;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import envoy.client.data.Settings;
import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>GeneralSettingsPane.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public class GeneralSettingsPane extends SettingsPane {
private static final Settings settings = Settings.getInstance();
/**
* @since Envoy Client v0.1-beta
*/
public GeneralSettingsPane() {
super("General");
final var vbox = new VBox();
// TODO: Support other value types
List.of("onCloseMode", "enterToSend")
.stream()
.map(settings.getItems()::get)
.map(i -> new SettingsCheckbox((SettingsItem<Boolean>) i))
.forEach(vbox.getChildren()::add);
final var combobox = new ComboBox<String>();
combobox.getItems().add("dark");
combobox.getItems().add("light");
combobox.setValue(settings.getCurrentTheme());
combobox.setOnAction(
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent(combobox.getValue())); });
vbox.getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>();
statusComboBox.getItems().setAll(UserStatus.values());
statusComboBox.setValue(UserStatus.ONLINE);
// TODO add action when value is changed
statusComboBox.setOnAction(e -> {});
vbox.getChildren().add(statusComboBox);
getChildren().add(vbox);
}
}

View File

@ -0,0 +1,32 @@
package envoy.client.ui.settings;
import javafx.event.ActionEvent;
import javafx.scene.control.CheckBox;
import envoy.client.data.SettingsItem;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsToggleButton.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class SettingsCheckbox extends CheckBox {
/**
* Creates an instance of {@link SettingsCheckbox}.
*
* @param settingsItem the {@link SettingsItem} whose values could be adapted
* @since Envoy Client v0.1-beta
*/
public SettingsCheckbox(SettingsItem<Boolean> settingsItem) {
super(settingsItem.getUserFriendlyName());
setSelected(settingsItem.get());
// "Schau, es hat sich behindert" - Kai, 2020
addEventHandler(ActionEvent.ACTION, e -> settingsItem.set(isSelected()));
}
}

View File

@ -0,0 +1,24 @@
package envoy.client.ui.settings;
import javafx.scene.layout.Pane;
/**
* Project: <strong>envoy-client</strong><br>
* File: <strong>SettingsPane.java</strong><br>
* Created: <strong>18.04.2020</strong><br>
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public abstract class SettingsPane extends Pane {
protected String title;
protected SettingsPane(String title) { this.title = title; }
/**
* @return the title of this settings pane
* @since Envoy Client v0.1-beta
*/
public String getTitle() { return title; }
}

View File

@ -0,0 +1,14 @@
/**
* This package contains classes used for representing the settings
* visually.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>package-info.java</strong><br>
* Created: <strong>19 Apr 2020</strong><br>
*
* @author Leon Hofmeister
* @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
package envoy.client.ui.settings;

View File

@ -0,0 +1,23 @@
/**
* This module contains all classes defining the client application of the Envoy
* project.
*
* @author Kai S. K. Engelbart
* @author Leon Hofmeister
* @author Maximilian K&auml;fer
* @since Envoy Client v0.1-beta
*/
module envoy {
requires transitive envoy.common;
requires transitive java.desktop;
requires transitive java.logging;
requires transitive java.prefs;
requires javafx.controls;
requires javafx.fxml;
requires javafx.base;
requires javafx.graphics;
opens envoy.client.ui to javafx.graphics, javafx.fxml;
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml;
}

Binary file not shown.

View File

@ -0,0 +1,3 @@
server=localhost
port=8080
localDB=.\\localDB

View File

@ -0,0 +1,87 @@
.button, .list-cell, .progress-bar * {
-fx-background-radius: 5.0em;
}
.context-menu, .context-menu > * {
-fx-background-radius: 15.0px;
/*TODO: solution below does not work */
-fx-background-color: transparent;
}
.menu-item {
-fx-background-radius: 15.0px;
}
.button:hover {
-fx-scale-x: 1.05;
-fx-scale-y: 1.05;
}
.label {
-fx-background-color: transparent;
}
.scroll-bar:horizontal, .scroll-bar:horizontal *, .scroll-bar:horizontal > *{
-fx-background-color: transparent;
-fx-text-fill: transparent;
}
.progress-bar{
-fx-progress-color: blue;
}
.online {
-fx-text-fill: limegreen;
}
.away {
-fx-text-fill: orangered;
}
.busy {
-fx-text-fill: red;
}
.offline {
-fx-text-fill: gray;
}
.received-message {
-fx-alignment: center-left;
-fx-background-radius: 4.0em;
-fx-text-alignment: right;
}
.own-message {
-fx-alignment: center-right;
-fx-background-radius: 4.0em;
-fx-text-alignment: left;
}
.unreadMessagesAmount {
-fx-alignment: center;
-fx-background-color: orange;
-fx-background-radius: 4.0em;
-fx-text-alignment: center;
}
#remainingCharsLabel {
-fx-text-fill: #00FF00;
-fx-background-color: transparent;
}
#infoLabel-success {
-fx-text-fill: #00FF00;
}
#infoLabel-info {
-fx-text-fill: yellow;
}
#infoLabel-warning {
-fx-text-fill: orange;
}
#infoLabel-error {
-fx-text-fill: red;
}

View File

@ -0,0 +1,39 @@
* {
-fx-text-fill: white;
}
.root {
-fx-background-color: black;
}
.button {
-fx-background-color: rgb(105.0,0.0,153.0);
}
.button:pressed {
-fx-background-color: darkviolet;
}
.button:disabled {
-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;
}
.list-cell:selected, .list-cell:selected > *, .menu-item:hover {
-fx-background-color: rgb(105.0,0.0,153.0);
}
.received-message {
-fx-background-color: gray;
}
.own-message {
-fx-background-color: #8fa88f;
}
.alert.information.dialog-pane, .alert.warning.dialog-pane, .alert.error.dialog-pane {
-fx-background-color: black;
}

View File

@ -0,0 +1,16 @@
.button{
-fx-background-color: orangered;
}
.list-cell:selected, .list-cell:selected > * {
-fx-background-color: orangered;
-fx-text-fill: black;
}
.received-message, .menu-item {
-fx-background-color: lightgray;
}
.own-message {
-fx-background-color: lightgreen;
}

View File

@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.geometry.Rectangle2D?>
<?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?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<GridPane fx:id="scene" hgap="5.0" maxHeight="-Infinity"
maxWidth="-Infinity" minHeight="400.0" minWidth="350.0"
prefHeight="400.0" prefWidth="600.0" vgap="2.0"
xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.ChatScene">
<columnConstraints>
<ColumnConstraints hgrow="NEVER" minWidth="60.0"
prefWidth="160.0" />
<ColumnConstraints hgrow="ALWAYS"
maxWidth="1.7976931348623157E308" minWidth="10.0" prefWidth="357.0" />
<ColumnConstraints hgrow="ALWAYS"
maxWidth="1.7976931348623157E308" minWidth="10.0" percentWidth="7.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="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="chatList" onMouseClicked="#chatListClicked"
prefHeight="211.0" prefWidth="300.0" GridPane.rowIndex="1"
GridPane.rowSpan="2147483647">
<GridPane.margin>
<Insets bottom="5.0" left="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="2.0" top="5.0" />
</padding>
<contextMenu>
<ContextMenu anchorLocation="CONTENT_TOP_LEFT">
<items>
<MenuItem fx:id="deleteContactMenuItem"
mnemonicParsing="false" onAction="#deleteContact" text="Delete" />
</items>
</ContextMenu>
</contextMenu>
</ListView>
<Label fx:id="contactLabel" prefHeight="27.0" prefWidth="134.0"
GridPane.columnSpan="2">
<GridPane.margin>
<Insets left="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Label>
<Button fx:id="settingsButton" mnemonicParsing="true"
onAction="#settingsButtonClicked" text="_Settings"
GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
GridPane.halignment="RIGHT" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="10.0" right="10.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Button>
<ListView fx:id="messageList" GridPane.columnIndex="1"
GridPane.columnSpan="2147483647" GridPane.rowIndex="1"
GridPane.rowSpan="2">
<GridPane.margin>
<Insets left="5.0" right="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="2.0" top="5.0" />
</padding>
</ListView>
<ButtonBar buttonMinWidth="40.0" GridPane.columnIndex="1"
GridPane.columnSpan="2147483647" GridPane.halignment="CENTER"
GridPane.rowIndex="5" GridPane.valignment="BOTTOM">
<GridPane.margin>
<Insets right="10.0" />
</GridPane.margin>
<buttons>
<Button fx:id="rotateButton" mnemonicParsing="false"
onAction="#doABarrelRoll">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Button>
<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"
onAction="#voiceButtonClicked">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Button>
<Button fx:id="postButton" defaultButton="true"
disable="true" mnemonicParsing="true" onAction="#postMessage"
text="_Post">
<tooltip>
<Tooltip anchorLocation="WINDOW_TOP_LEFT" autoHide="true"
maxWidth="350.0"
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 &quot;Alt&quot; + &quot;P&quot;."
wrapText="true" />
</tooltip>
<contextMenu>
<ContextMenu anchorLocation="CONTENT_TOP_LEFT">
<items>
<MenuItem mnemonicParsing="false"
onAction="#copyAndPostMessage" text="Copy and Send" />
</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"
onInputMethodTextChanged="#messageTextUpdated"
onKeyPressed="#checkPostConditions" onKeyTyped="#checkKeyCombination"
prefHeight="200.0" prefWidth="200.0" wrapText="true"
GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
GridPane.rowIndex="4">
<GridPane.margin>
<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" />
</opaqueInsets>
</TextArea>
<Button mnemonicParsing="true"
onAction="#addContactButtonClicked" text="_Add Contacts"
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="10.0" left="10.0" right="5.0" />
</GridPane.margin>
</Button>
<Label id="remainingCharsLabel" fx:id="remainingChars"
ellipsisString="" maxHeight="30.0" maxWidth="180.0" prefHeight="30.0"
prefWidth="180.0" text="remaining chars: 0/x" textFill="LIME"
textOverrun="LEADING_WORD_ELLIPSIS" visible="false"
GridPane.columnIndex="1" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" top="5.0" />
</padding>
<opaqueInsets>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</opaqueInsets>
<tooltip>
<Tooltip
text="Shows how many chars you can still enter in this message"
wrapText="true" />
</tooltip>
</Label>
<Label fx:id="infoLabel" text="Something happened"
textFill="#faa007" visible="false" wrapText="true"
GridPane.columnIndex="1">
<GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Label>
<ImageView fx:id="attachmentView" pickOnBounds="true"
preserveRatio="true" visible="false" GridPane.columnIndex="1"
GridPane.columnSpan="2147483647" GridPane.halignment="RIGHT"
GridPane.rowIndex="3">
<viewport>
<Rectangle2D height="20.0" width="20.0" />
</viewport>
<GridPane.margin>
<Insets bottom="5.0" right="10.0" top="5.0" />
</GridPane.margin>
</ImageView>
</children>
</GridPane>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import envoy.client.ui.ClearableTextField?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.ContactSearchScene">
<children>
<HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0">
<children>
<ClearableTextField fx:id="searchBar"
prefWidth="310.0">
<textField onInputMethodTextChanged="#sendRequest"
onKeyTyped="#sendRequest" prefColumnCount="22"
promptText="Enter username to search for">
</textField>
<HBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="15.0" />
</HBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<tooltip>
<Tooltip
text="Enter a name. If an account by that name exists, it will be displayed below."
wrapText="true" />
</tooltip>
</ClearableTextField>
<Button mnemonicParsing="false"
onAction="#newGroupButtonClicked" prefHeight="26.0"
prefWidth="139.0" text="New Group">
<HBox.margin>
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
</HBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Button>
</children>
</HBox>
<ListView fx:id="chatList"
onMouseClicked="#chatListClicked" prefHeight="314.0"
prefWidth="600.0">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<VBox.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</VBox.margin>
</ListView>
<Button cancelButton="true" mnemonicParsing="true"
onAction="#backButtonClicked" text="_Back">
<VBox.margin>
<Insets bottom="10.0" left="10.0" />
</VBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<tooltip>
<Tooltip autoHide="true"
text="Takes you back to the screen where you can chat with others"
wrapText="true" />
</tooltip>
</Button>
</children>
</VBox>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import envoy.client.ui.ClearableTextField?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.GroupCreationScene">
<children>
<HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0">
<children>
<ClearableTextField fx:id="groupNameField">
<textField prefColumnCount="22"
promptText="Enter Group Name"
onInputMethodTextChanged="#textUpdated" onKeyTyped="#textUpdated" />
<HBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</HBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<tooltip>
<Tooltip
text="Enter something. A group with this name will be created."
wrapText="true" />
</tooltip>
</ClearableTextField>
</children>
</HBox>
<Label text="Choose Members:">
<font>
<Font size="16.0" />
</font>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Label>
<ListView fx:id="chatList"
onMouseClicked="#chatListClicked" prefHeight="314.0"
prefWidth="600.0">
<VBox.margin>
<Insets bottom="5.0" left="10.0" right="10.0" top="5.0" />
</VBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</ListView>
<BorderPane prefHeight="50.0">
<left>
<Button cancelButton="true" mnemonicParsing="true"
onAction="#backButtonClicked" text="_Back"
BorderPane.alignment="CENTER">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<tooltip>
<Tooltip autoHide="true"
text="Takes you back to the screen where you can chat with others"
wrapText="true" />
</tooltip>
<BorderPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</BorderPane.margin>
</Button>
</left>
<right>
<Button fx:id="createButton" alignment="CENTER_RIGHT"
defaultButton="true" disable="true" mnemonicParsing="false"
onAction="#createButtonClicked" text="Create"
BorderPane.alignment="CENTER">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<BorderPane.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</BorderPane.margin>
</Button>
</right>
</BorderPane>
</children>
</VBox>

View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import envoy.client.ui.ClearableTextField?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox prefHeight="206.0" prefWidth="440.0"
xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.LoginScene">
<children>
<Label text="User Login">
<font>
<Font size="26.0" />
</font>
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</Label>
<GridPane hgap="5.0" vgap="10.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES"
minWidth="10.0" percentWidth="40.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES"
minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0"
vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0"
vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0"
vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label text="User Name:">
<GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</Label>
<Label text="Password:" GridPane.rowIndex="1">
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
<GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin>
</Label>
<Label fx:id="repeatPasswordLabel" text="Repeat Password:"
visible="false" GridPane.rowIndex="2">
<GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin>
<padding>
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
</padding>
</Label>
<ClearableTextField fx:id="userTextField"
GridPane.columnIndex="1">
<GridPane.margin>
<Insets bottom="10.0" left="5.0" right="5.0" top="5.0" />
</GridPane.margin>
</ClearableTextField>
<PasswordField fx:id="passwordField"
GridPane.columnIndex="1" GridPane.rowIndex="1">
<GridPane.margin>
<Insets bottom="10.0" left="5.0" right="5.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</PasswordField>
<PasswordField fx:id="repeatPasswordField"
visible="false" GridPane.columnIndex="1" GridPane.rowIndex="2">
<GridPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="10.0" />
</GridPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</PasswordField>
</children>
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</GridPane>
<CheckBox fx:id="registerCheckBox" mnemonicParsing="true"
onAction="#registerCheckboxChanged" prefHeight="17.0"
prefWidth="181.0" text="_Register">
<VBox.margin>
<Insets left="5.0" right="3.0" />
</VBox.margin>
</CheckBox>
<Label fx:id="connectionLabel">
<VBox.margin>
<Insets left="5.0" />
</VBox.margin>
</Label>
<BorderPane prefWidth="200.0">
<left>
<Button cancelButton="true" mnemonicParsing="false"
onAction="#abortLogin" text="Close" BorderPane.alignment="CENTER">
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<BorderPane.margin>
<Insets />
</BorderPane.margin>
</Button>
</left>
<center>
<Button mnemonicParsing="false"
onAction="#offlineModeButtonPressed" text="Offline mode"
BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets />
</BorderPane.margin>
</Button>
</center>
<right>
<Button defaultButton="true" mnemonicParsing="false"
onAction="#loginButtonPressed" text="Login"
BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets />
</BorderPane.margin>
</Button>
</right>
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" />
</VBox.margin>
</BorderPane>
</children>
</VBox>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="TOP_RIGHT" maxHeight="-Infinity"
maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
prefHeight="400.0" prefWidth="600.0"
xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="envoy.client.ui.controller.SettingsScene">
<children>
<HBox prefHeight="389.0" prefWidth="600.0">
<children>
<ListView fx:id="settingsList"
onMouseClicked="#settingsListClicked" prefHeight="200.0"
prefWidth="200.0">
<opaqueInsets>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</opaqueInsets>
<HBox.margin>
<Insets bottom="10.0" left="10.0" right="5.0" top="10.0" />
</HBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</ListView>
<TitledPane fx:id="titledPane" collapsible="false"
prefHeight="325.0" prefWidth="300.0">
<HBox.margin>
<Insets bottom="10.0" left="5.0" right="10.0" top="10.0" />
</HBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</TitledPane>
</children>
</HBox>
<Button defaultButton="true" mnemonicParsing="true"
onMouseClicked="#backButtonClicked" text="_Back">
<opaqueInsets>
<Insets />
</opaqueInsets>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</Button>
</children>
</VBox>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB