Merge pull request 'Add Ability to Logout' (#50) from f/logout into develop

Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/50
Reviewed-by: kske <kai@kske.dev>
This commit is contained in:
Kai S. K. Engelbart 2020-09-27 15:48:12 +02:00
commit c7ee545ee2
18 changed files with 378 additions and 55 deletions

View File

@ -56,4 +56,11 @@ public final class Cache<T> implements Consumer<T>, Serializable {
elements.forEach(processor::accept); elements.forEach(processor::accept);
elements.clear(); elements.clear();
} }
/**
* Clears this cache of all stored elements.
*
* @since Envoy Client v0.2-beta
*/
public void clear() { elements.clear(); }
} }

View File

@ -6,7 +6,7 @@ import java.util.*;
/** /**
* Stores a heterogeneous map of {@link Cache} objects with different type * Stores a heterogeneous map of {@link Cache} objects with different type
* parameters. * parameters.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
@ -18,7 +18,7 @@ public final class CacheMap implements Serializable {
/** /**
* Adds a cache to the map. * Adds a cache to the map.
* *
* @param <T> the type accepted by the cache * @param <T> the type accepted by the cache
* @param key the class that maps to the cache * @param key the class that maps to the cache
* @param cache the cache to store * @param cache the cache to store
@ -28,7 +28,7 @@ public final class CacheMap implements Serializable {
/** /**
* Returns a cache mapped by a class. * Returns a cache mapped by a class.
* *
* @param <T> the type accepted by the cache * @param <T> the type accepted by the cache
* @param key the class that maps to the cache * @param key the class that maps to the cache
* @return the cache * @return the cache
@ -38,7 +38,7 @@ public final class CacheMap implements Serializable {
/** /**
* Returns a cache mapped by a class or any of its subclasses. * Returns a cache mapped by a class or any of its subclasses.
* *
* @param <T> the type accepted by the cache * @param <T> the type accepted by the cache
* @param key the class that maps to the cache * @param key the class that maps to the cache
* @return the cache * @return the cache
@ -47,7 +47,7 @@ public final class CacheMap implements Serializable {
public <T> Cache<? super T> getApplicable(Class<T> key) { public <T> Cache<? super T> getApplicable(Class<T> key) {
Cache<? super T> cache = get(key); Cache<? super T> cache = get(key);
if (cache == null) if (cache == null)
for (var e : map.entrySet()) for (final var e : map.entrySet())
if (e.getKey().isAssignableFrom(key)) if (e.getKey().isAssignableFrom(key))
cache = (Cache<? super T>) e.getValue(); cache = (Cache<? super T>) e.getValue();
return cache; return cache;
@ -58,4 +58,11 @@ public final class CacheMap implements Serializable {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public Map<Class<?>, Cache<?>> getMap() { return map; } public Map<Class<?>, Cache<?>> getMap() { return map; }
/**
* Clears the caches of this map of any values.
*
* @since Envoy Client v0.2-beta
*/
public void clear() { map.values().forEach(Cache::clear); }
} }

View File

@ -9,7 +9,7 @@ import java.util.logging.*;
import javafx.collections.*; import javafx.collections.*;
import envoy.client.event.EnvoyCloseEvent; import envoy.client.event.*;
import envoy.data.*; import envoy.data.*;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
import envoy.event.*; import envoy.event.*;
@ -39,6 +39,10 @@ public final class LocalDB implements EventListener {
private CacheMap cacheMap = new CacheMap(); private CacheMap cacheMap = new CacheMap();
private String authToken; private String authToken;
// Auto save timer
private Timer autoSaver;
private boolean autoSaveRestart = true;
// State management // State management
private Instant lastSync = Instant.EPOCH; private Instant lastSync = Instant.EPOCH;
@ -168,7 +172,14 @@ public final class LocalDB implements EventListener {
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void initAutoSave() { public void initAutoSave() {
new Timer("LocalDB Autosave", true).schedule(new TimerTask() {
// A logout happened so the timer should be restarted
if (autoSaveRestart) {
autoSaver = new Timer("LocalDB Autosave", true);
autoSaveRestart = false;
}
autoSaver.schedule(new TimerTask() {
@Override @Override
public void run() { save(); } public void run() { save(); }
@ -244,6 +255,24 @@ public final class LocalDB implements EventListener {
@Event @Event
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); } private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
/**
* Deletes all associations to the current user.
*
* @since Envoy Client v0.2-beta
*/
@Event(eventType = Logout.class, priority = 100)
private void onLogout() {
autoSaver.cancel();
autoSaveRestart = true;
lastLoginFile.delete();
userFile = null;
user = null;
authToken = null;
chats.clear();
lastSync = Instant.EPOCH;
cacheMap.clear();
}
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their * @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys * user names as keys

View File

@ -88,6 +88,8 @@ public final class Settings implements EventListener {
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location", new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location",
"The location where files will be saved to")); "The location where files will be saved to"));
items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?", "Should downloads be saved without asking?")); items.putIfAbsent("autoSaveDownloads", new SettingsItem<>(false, "Save without asking?", "Should downloads be saved without asking?"));
items.putIfAbsent("askForConfirmation",
new SettingsItem<>(true, "Ask for confirmation", "Will ask for confirmation before doing certain things"));
} }
/** /**
@ -178,6 +180,25 @@ public final class Settings implements EventListener {
*/ */
public void setHideOnClose(boolean hideOnClose) { ((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose); } public void setHideOnClose(boolean hideOnClose) { ((SettingsItem<Boolean>) items.get("hideOnClose")).set(hideOnClose); }
/**
* @return whether a confirmation dialog should be displayed before certain
* actions
* @since Envoy Client v0.2-alpha
*/
public Boolean isAskForConfirmation() { return (Boolean) items.get("askForConfirmation").get(); }
/**
* Changes the behavior of calling certain functionality by displaying a
* confirmation dialog before executing it.
*
* @param askForConfirmation whether confirmation dialogs should be displayed
* before certain actions
* @since Envoy Client v0.2-alpha
*/
public void setAskForConfirmation(boolean askForConfirmation) {
((SettingsItem<Boolean>) items.get("askForConfirmation")).set(askForConfirmation);
}
/** /**
* @return the items * @return the items
*/ */

View File

@ -17,6 +17,22 @@ public final class SystemCommandBuilder {
private String description; private String description;
private int relevance; private int relevance;
private final SystemCommandMap commandsMap;
/**
* Creates a new {@code SystemCommandsBuilder} without underlying
* {@link SystemCommandMap}.
*
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder() { this(null); }
/**
* @param commandsMap the map to use when calling build (optional)
* @since Envoy Client v0.2-beta
*/
public SystemCommandBuilder(SystemCommandMap commandsMap) { this.commandsMap = commandsMap; }
/** /**
* @param numberOfArguments the numberOfArguments to set * @param numberOfArguments the numberOfArguments to set
* @return this {@code SystemCommandBuilder} * @return this {@code SystemCommandBuilder}
@ -120,6 +136,7 @@ public final class SystemCommandBuilder {
/** /**
* Builds a {@code SystemCommand} based upon the previously entered data.<br> * Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must * At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must
* not be. * not be.
* *
@ -136,4 +153,78 @@ public final class SystemCommandBuilder {
if (reset) reset(); if (reset) reset();
return sc; return sc;
} }
/**
* Builds a {@code SystemCommand} based upon the previously entered data.
* Automatically adds the built object to the given map.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand build(String command) { return build(command, true); }
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* {@code SystemCommand#numberOfArguments} will be set to 0, regardless of the
* previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand buildNoArg(String command) {
numberOfArguments = 0;
return build(command, true);
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* {@code SystemCommand#numberOfArguments} will be set to use the rest of the
* string as argument, regardless of the previous value.<br>
* At the end, this {@code SystemCommandBuilder} will be reset.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand buildRemainingArg(String command) {
numberOfArguments = -1;
return build(command, true);
}
/**
* Builds a {@code SystemCommand} based upon the previously entered data.<br>
* Automatically adds the built object to the given map.
* At the end, this {@code SystemCommandBuilder} <b>can</b> be reset but must
* not be.
*
* @param command the command under which to store the SystemCommand in the
* {@link SystemCommandMap}
* @param reset whether this {@code SystemCommandBuilder} should be reset
* afterwards.<br>
* This can be useful if another command wants to execute
* something
* similar
* @return the built {@code SystemCommand}
* @throws NullPointerException if no map has been assigned to this builder
* @since Envoy Client v0.2-beta
*/
public SystemCommand build(String command, boolean reset) {
final var sc = new SystemCommand(action, numberOfArguments, defaults, description);
sc.setRelevance(relevance);
if (commandsMap != null) commandsMap.add(command, sc);
else throw new NullPointerException("No map in SystemCommandsBuilder present");
if (reset) reset();
return sc;
}
} }

View File

@ -14,13 +14,13 @@ import envoy.util.EnvoyLog;
* @author Leon Hofmeister * @author Leon Hofmeister
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public final class SystemCommandsMap { public final class SystemCommandMap {
private final Map<String, SystemCommand> systemCommands = new HashMap<>(); private final Map<String, SystemCommand> systemCommands = new HashMap<>();
private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$"); private final Pattern commandPattern = Pattern.compile("^[a-zA-Z0-9_:!\\(\\)\\?\\.\\,\\;\\-]+$");
private static final Logger logger = EnvoyLog.getLogger(SystemCommandsMap.class); private static final Logger logger = EnvoyLog.getLogger(SystemCommandMap.class);
/** /**
* Adds a new command to the map if the command name is valid. * Adds a new command to the map if the command name is valid.
@ -29,7 +29,7 @@ public final class SystemCommandsMap {
* given action * given action
* @param systemCommand the command to add - can be built using * @param systemCommand the command to add - can be built using
* {@link SystemCommandBuilder} * {@link SystemCommandBuilder}
* @see SystemCommandsMap#isValidKey(String) * @see SystemCommandMap#isValidKey(String)
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void add(String command, SystemCommand systemCommand) { public void add(String command, SystemCommand systemCommand) {
@ -44,7 +44,7 @@ public final class SystemCommandsMap {
* map). * map).
* <p> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandsMap systemCommands = new SystemCommandsMap();}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code Button button = new Button();} * {@code Button button = new Button();}
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br> * {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
* {@code ....}<br> * {@code ....}<br>
@ -128,7 +128,7 @@ public final class SystemCommandsMap {
* map). * map).
* <p> * <p>
* Usage example:<br> * Usage example:<br>
* {@code SystemCommandsMap systemCommands = new SystemCommandsMap();}<br> * {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code Button button = new Button();}<br> * {@code Button button = new Button();}<br>
* {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br> * {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br>
* {@code ....}<br> * {@code ....}<br>

View File

@ -0,0 +1,14 @@
package envoy.client.event;
import envoy.event.Event.Valueless;
/**
* Indicates that a logout has been requested.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class Logout extends Valueless {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,36 @@
package envoy.client.helper;
import javafx.scene.control.*;
import envoy.client.data.Settings;
/**
* Provides methods that are commonly used for alerts.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class AlertHelper {
private AlertHelper() {}
/**
* Asks for a confirmation dialog if {@link Settings#isAskForConfirmation()}
* returns {@code true}.
* Immediately executes the action if no dialog was requested or the dialog was
* exited with a confirmation.
* Does nothing if the dialog was closed without clicking on OK.
*
* @param alert the (customized) alert to show. <strong>Should not be shown
* already</strong>
* @param action the action to perform in case of success
* @since Envoy Client v0.2-beta
*/
public static void confirmAction(Alert alert, Runnable action) {
alert.setHeight(225);
alert.setWidth(400);
alert.setHeaderText("");
if (Settings.getInstance().isAskForConfirmation()) alert.showAndWait().filter(ButtonType.OK::equals).ifPresent(bu -> action.run());
else action.run();
}
}

View File

@ -0,0 +1,57 @@
package envoy.client.helper;
import java.util.logging.Level;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.*;
import envoy.client.event.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus;
/**
* Simplifies shutdown actions.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class ShutdownHelper {
private ShutdownHelper() {}
/**
* Exits Envoy or minimizes it, depending on the current state of
* {@link Settings#isHideOnClose()}.
*
* @since Envoy Client v0.2-beta
*/
public static void exit() {
if (Settings.getInstance().isHideOnClose()) Context.getInstance().getStage().setIconified(true);
else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
System.exit(0);
}
}
/**
* Logs the current user out and reopens
* {@link envoy.client.ui.controller.LoginScene}.
*
* @since Envoy Client v0.2-beta
*/
public static void logout() {
final var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Logout?");
alert.setContentText("Are you sure you want to log out?");
AlertHelper.confirmAction(alert, () -> {
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
EventBus.getInstance().dispatch(new Logout());
Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE);
});
}
}

View File

@ -0,0 +1,9 @@
/**
* Provides helper methods that reduce boilerplate code.
*
* @author Leon Hofmeister
* @author Kai S. K. Engelbert
* @author Maximilian K&auml;fer
* @since Envoy Client v0.2-beta
*/
package envoy.client.helper;

View File

@ -100,7 +100,6 @@ public final class Client implements EventListener, Closeable {
} }
online = true; online = true;
logger.log(Level.INFO, "Handshake completed."); logger.log(Level.INFO, "Handshake completed.");
} }
@ -183,8 +182,14 @@ public final class Client implements EventListener, Closeable {
if (online) { if (online) {
logger.log(Level.INFO, "Closing connection..."); logger.log(Level.INFO, "Closing connection...");
try { try {
// The sender must be reset as otherwise the handshake is immediately closed
sender = null;
online = false;
socket.close(); socket.close();
} catch (final IOException e) {} } catch (final IOException e) {
logger.log(Level.WARNING, "Failed to close socket: ", e);
}
} }
} }
@ -212,6 +217,7 @@ public final class Client implements EventListener, Closeable {
/** /**
* @return the {@link Receiver} used by this {@link Client} * @return the {@link Receiver} used by this {@link Client}
* @since v0.2-alpha
*/ */
public Receiver getReceiver() { return receiver; } public Receiver getReceiver() { return receiver; }

View File

@ -36,6 +36,7 @@ public final class Receiver extends Thread {
public Receiver(InputStream in) { public Receiver(InputStream in) {
super("Receiver"); super("Receiver");
this.in = in; this.in = in;
setDaemon(true);
} }
/** /**

View File

@ -12,6 +12,7 @@ import javafx.stage.Stage;
import envoy.client.data.Settings; import envoy.client.data.Settings;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.client.helper.ShutdownHelper;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import dev.kske.eventbus.*; import dev.kske.eventbus.*;
@ -92,6 +93,7 @@ public final class SceneContext implements EventListener {
* @since Envoy Client v0.1-beta * @since Envoy Client v0.1-beta
*/ */
public void load(SceneInfo sceneInfo) { public void load(SceneInfo sceneInfo) {
EnvoyLog.getLogger(SceneContext.class).log(Level.FINER, "Loading scene " + sceneInfo);
loader.setRoot(null); loader.setRoot(null);
loader.setController(null); loader.setController(null);
@ -103,18 +105,17 @@ public final class SceneContext implements EventListener {
sceneStack.push(scene); sceneStack.push(scene);
stage.setScene(scene); stage.setScene(scene);
// Adding the option to exit Linux-like with "Control" + "Q" // Add the option to exit Linux-like with "Control" + "Q"
scene.getAccelerators() scene.getAccelerators().put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
.put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN),
() -> { // Add the option to logout using "Control"+"Shift"+"L" if not in login scene
// Presumably no Settings are loaded in the login scene, hence Envoy is closed if (sceneInfo != SceneInfo.LOGIN_SCENE) scene.getAccelerators()
// directly .put(new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN), ShutdownHelper::logout);
if (sceneInfo != SceneInfo.LOGIN_SCENE && settings.isHideOnClose()) stage.setIconified(true);
else { // Add the option to open the settings scene with "Control"+"S", if being in
EventBus.getInstance().dispatch(new EnvoyCloseEvent()); // chat scene
System.exit(0); if (sceneInfo.equals(SceneInfo.CHAT_SCENE))
} scene.getAccelerators().put(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN), () -> load(SceneInfo.SETTINGS_SCENE));
});
// The LoginScene is the only scene not intended to be resized // The LoginScene is the only scene not intended to be resized
// As strange as it seems, this is needed as otherwise the LoginScene won't be // As strange as it seems, this is needed as otherwise the LoginScene won't be
@ -163,6 +164,12 @@ public final class SceneContext implements EventListener {
} }
} }
@Event(eventType = Logout.class, priority = 150)
private void onLogout() {
sceneStack.clear();
controllerStack.clear();
}
@Event(priority = 150, eventType = ThemeChangeEvent.class) @Event(priority = 150, eventType = ThemeChangeEvent.class)
private void onThemeChange() { applyCSS(); } private void onThemeChange() { applyCSS(); }

View File

@ -11,7 +11,7 @@ import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent; import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene; import envoy.client.ui.controller.LoginScene;
@ -22,10 +22,8 @@ import envoy.event.*;
import envoy.exception.EnvoyException; import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog; import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus;
/** /**
* Handles application startup and shutdown. * Handles application startup.
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
@ -55,6 +53,8 @@ public final class Startup extends Application {
*/ */
@Override @Override
public void start(Stage stage) throws Exception { public void start(Stage stage) throws Exception {
// Initialize config and logger
try { try {
config.loadAll(Startup.class, "client.properties", getParameters().getRaw().toArray(new String[0])); config.loadAll(Startup.class, "client.properties", getParameters().getRaw().toArray(new String[0]));
EnvoyLog.initialize(config); EnvoyLog.initialize(config);
@ -67,7 +67,7 @@ public final class Startup extends Application {
// Initialize the local database // Initialize the local database
try { try {
var localDBFile = new File(config.getHomeDirectory(), config.getServer()); final var localDBFile = new File(config.getHomeDirectory(), config.getServer());
logger.info("Initializing LocalDB at " + localDBFile); logger.info("Initializing LocalDB at " + localDBFile);
localDB = new LocalDB(localDBFile); localDB = new LocalDB(localDBFile);
} catch (IOException | EnvoyException e) { } catch (IOException | EnvoyException e) {
@ -114,7 +114,6 @@ public final class Startup extends Application {
cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>()); cacheMap.put(MessageStatusChange.class, new Cache<MessageStatusChange>());
cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>()); cacheMap.put(GroupMessageStatusChange.class, new Cache<GroupMessageStatusChange>());
try { try {
final var client = context.getClient();
client.performHandshake(credentials, cacheMap); client.performHandshake(credentials, cacheMap);
if (client.isOnline()) { if (client.isOnline()) {
loadChatScene(); loadChatScene();
@ -208,13 +207,8 @@ public final class Startup extends Application {
if (StatusTrayIcon.isSupported()) { if (StatusTrayIcon.isSupported()) {
// Configure hide on close // Exit or minimize the stage when a close request occurs
stage.setOnCloseRequest(e -> { stage.setOnCloseRequest(e -> { ShutdownHelper.exit(); if (Settings.getInstance().isHideOnClose()) e.consume(); });
if (Settings.getInstance().isHideOnClose()) {
stage.setIconified(true);
e.consume();
} else EventBus.getInstance().dispatch(new EnvoyCloseEvent());
});
// Initialize status tray icon // Initialize status tray icon
final var trayIcon = new StatusTrayIcon(stage); final var trayIcon = new StatusTrayIcon(stage);

View File

@ -6,6 +6,7 @@ import java.awt.TrayIcon.MessageType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.helper.ShutdownHelper;
import envoy.client.util.IconUtil; import envoy.client.util.IconUtil;
import envoy.data.Message; import envoy.data.Message;
@ -53,7 +54,7 @@ public final class StatusTrayIcon implements EventListener {
final PopupMenu popup = new PopupMenu(); final PopupMenu popup = new PopupMenu();
final MenuItem exitMenuItem = new MenuItem("Exit"); final MenuItem exitMenuItem = new MenuItem("Exit");
exitMenuItem.addActionListener(evt -> { Platform.exit(); System.exit(0); }); exitMenuItem.addActionListener(evt -> ShutdownHelper.exit());
popup.add(exitMenuItem); popup.add(exitMenuItem);
trayIcon.setPopupMenu(popup); trayIcon.setPopupMenu(popup);

View File

@ -28,8 +28,10 @@ import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; import envoy.client.data.audio.AudioRecorder;
import envoy.client.data.commands.*; import envoy.client.data.commands.*;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.client.helper.ShutdownHelper;
import envoy.client.net.*; import envoy.client.net.*;
import envoy.client.ui.*; import envoy.client.ui.*;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.control.*; import envoy.client.ui.control.*;
import envoy.client.ui.listcell.*; import envoy.client.ui.listcell.*;
import envoy.client.util.*; import envoy.client.util.*;
@ -142,7 +144,7 @@ public final class ChatScene implements EventListener, Restorable {
private final WriteProxy writeProxy = context.getWriteProxy(); private final WriteProxy writeProxy = context.getWriteProxy();
private final SceneContext sceneContext = context.getSceneContext(); private final SceneContext sceneContext = context.getSceneContext();
private final AudioRecorder recorder = new AudioRecorder(); private final AudioRecorder recorder = new AudioRecorder();
private final SystemCommandsMap messageTextAreaCommands = new SystemCommandsMap(); private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
private final Tooltip onlyIfOnlineTooltip = new Tooltip("You need to be online to do this"); private final Tooltip onlyIfOnlineTooltip = new Tooltip("You need to be online to do this");
private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20); private static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
@ -303,20 +305,37 @@ public final class ChatScene implements EventListener, Restorable {
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43)); else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
} }
@Event(eventType = Logout.class, priority = 200)
private void onLogout() { eventBus.removeListener(this); }
/** /**
* Initializes all {@code SystemCommands} used in {@code ChatScene}. * Initializes all {@code SystemCommands} used in {@code ChatScene}.
* *
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
private void initializeSystemCommandsMap() { private void initializeSystemCommandsMap() {
final var builder = new SystemCommandBuilder(); final var builder = new SystemCommandBuilder(messageTextAreaCommands);
// Do A Barrel roll initialization // Do A Barrel roll initialization
final var random = new Random(); final var random = new Random();
builder.setAction(text -> doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1)))) builder.setAction(text -> doABarrelRoll(Integer.parseInt(text.get(0)), Double.parseDouble(text.get(1))))
.setDefaults(Integer.toString(random.nextInt(3) + 1), Double.toString(random.nextDouble() * 3 + 1)) .setDefaults(Integer.toString(random.nextInt(3) + 1), Double.toString(random.nextDouble() * 3 + 1))
.setDescription("See for yourself :)") .setDescription("See for yourself :)")
.setNumberOfArguments(2); .setNumberOfArguments(2)
messageTextAreaCommands.add("DABR", builder.build()); .build("dabr");
// Logout initialization
builder.setAction(text -> ShutdownHelper.logout()).setNumberOfArguments(0).setDescription("Logs you out.").build("logout");
// Exit initialization
builder.setAction(text -> ShutdownHelper.exit()).setNumberOfArguments(0).setDescription("Exits the program").build("exit", false);
builder.build("q");
// Open settings scene initialization
builder.setAction(text -> sceneContext.load(SceneInfo.SETTINGS_SCENE))
.setNumberOfArguments(0)
.setDescription("Opens the settings screen")
.build("settings");
} }
@Override @Override

View File

@ -9,6 +9,7 @@ import javafx.scene.control.Alert.AlertType;
import envoy.client.data.Context; import envoy.client.data.Context;
import envoy.client.event.BackEvent; import envoy.client.event.BackEvent;
import envoy.client.helper.AlertHelper;
import envoy.client.net.Client; import envoy.client.net.Client;
import envoy.client.ui.control.ContactControl; import envoy.client.ui.control.ContactControl;
import envoy.client.ui.listcell.ListCellFactory; import envoy.client.ui.listcell.ListCellFactory;
@ -53,6 +54,7 @@ public class ContactSearchTab implements EventListener {
private void initialize() { private void initialize() {
eventBus.registerListener(this); eventBus.registerListener(this);
userList.setCellFactory(new ListCellFactory<>(ContactControl::new)); userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
alert.setTitle("Add User?");
} }
@Event @Event
@ -104,16 +106,23 @@ public class ContactSearchTab implements EventListener {
final var user = userList.getSelectionModel().getSelectedItem(); final var user = userList.getSelectionModel().getSelectedItem();
if (user != null) { if (user != null) {
currentlySelectedUser = user; currentlySelectedUser = user;
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD); alert.setContentText("Add user " + currentlySelectedUser.getName() + " to your contacts?");
// Sends the event to the server AlertHelper.confirmAction(alert, this::addAsContact);
client.send(event);
// Removes the chosen user and updates the UI
userList.getItems().remove(currentlySelectedUser);
eventBus.dispatch(event);
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
} }
} }
private void addAsContact() {
// Sends the event to the server
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
client.send(event);
// Removes the chosen user and updates the UI
userList.getItems().remove(currentlySelectedUser);
eventBus.dispatch(event);
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
}
@FXML @FXML
private void backButtonClicked() { eventBus.dispatch(new BackEvent()); } private void backButtonClicked() { eventBus.dispatch(new BackEvent()); }
} }

View File

@ -4,6 +4,7 @@ import javafx.scene.control.*;
import envoy.client.data.SettingsItem; import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.helper.ShutdownHelper;
import envoy.data.User.UserStatus; import envoy.data.User.UserStatus;
import dev.kske.eventbus.EventBus; import dev.kske.eventbus.EventBus;
@ -24,23 +25,30 @@ public final class GeneralSettingsPane extends SettingsPane {
// TODO: Support other value types // TODO: Support other value types
final var settingsItems = settings.getItems(); final var settingsItems = settings.getItems();
final var hideOnCloseCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose")); final var hideOnCloseCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("hideOnClose"));
hideOnCloseCheckbox.setTooltip(new Tooltip("If selected, Envoy will still be present in the task bar when closed.")); final var hideOnCloseTooltip = new Tooltip("If selected, Envoy will still be present in the task bar when closed.");
hideOnCloseTooltip.setWrapText(true);
hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip);
getChildren().add(hideOnCloseCheckbox); getChildren().add(hideOnCloseCheckbox);
final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend")); final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
final var enterToSendTooltip = new Tooltip( final var enterToSendTooltip = new Tooltip(
"If selected, messages can be sent pressing \"Enter\". They can always be sent by pressing \"Ctrl\" + \"Enter\""); "When selected, messages can be sent pressing \"Enter\". A line break can be inserted by pressing \"Ctrl\" + \"Enter\". Else it will be the other way around.");
enterToSendTooltip.setWrapText(true); enterToSendTooltip.setWrapText(true);
enterToSendCheckbox.setTooltip(enterToSendTooltip); enterToSendCheckbox.setTooltip(enterToSendTooltip);
getChildren().add(enterToSendCheckbox); getChildren().add(enterToSendCheckbox);
final var askForConfirmationCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("askForConfirmation"));
final var askForConfirmationTooltip = new Tooltip("When selected, nothing will prompt a confirmation dialog");
askForConfirmationTooltip.setWrapText(true);
askForConfirmationCheckbox.setTooltip(askForConfirmationTooltip);
getChildren().add(askForConfirmationCheckbox);
final var combobox = new ComboBox<String>(); final var combobox = new ComboBox<String>();
combobox.getItems().add("dark"); combobox.getItems().add("dark");
combobox.getItems().add("light"); combobox.getItems().add("light");
combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in.")); combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
combobox.setValue(settings.getCurrentTheme()); combobox.setValue(settings.getCurrentTheme());
combobox.setOnAction( combobox.setOnAction(e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); });
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); });
getChildren().add(combobox); getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>(); final var statusComboBox = new ComboBox<UserStatus>();
@ -50,5 +58,12 @@ public final class GeneralSettingsPane extends SettingsPane {
// TODO add action when value is changed // TODO add action when value is changed
statusComboBox.setOnAction(e -> {}); statusComboBox.setOnAction(e -> {});
getChildren().add(statusComboBox); getChildren().add(statusComboBox);
final var logoutButton = new Button("Logout");
logoutButton.setOnAction(e -> ShutdownHelper.logout());
final var logoutTooltip = new Tooltip("Brings you back to the login screen and removes \"remember me\" status from this account");
logoutTooltip.setWrapText(true);
logoutButton.setTooltip(logoutTooltip);
getChildren().add(logoutButton);
} }
} }