Improved SystemCommand mechanism, added Alert- and ShutdownHelper, and

... added askForConfirmation option
This commit is contained in:
Leon Hofmeister 2020-09-23 17:03:32 +02:00
parent 758e52e030
commit 2d9283551a
Signed by: delvh
GPG Key ID: 3DECE05F6D9A647C
13 changed files with 260 additions and 49 deletions

View File

@ -212,6 +212,13 @@ public final class LocalDB implements EventListener {
@Event
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
/**
* Deletes the file that stores the "remember me" feature.
*
* @since Envoy Client v0.2-beta
*/
public void deleteLoginFile() { lastLoginFile.delete(); }
/**
* @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys

View File

@ -92,6 +92,8 @@ public final class Settings implements EventListener {
new SettingsItem<>(new File(System.getProperty("user.home") + "/Downloads/"), "Download location",
"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("askForConfirmation",
new SettingsItem<>(true, "Ask for confirmation", "Will ask for confirmation before doing certain things"));
}
/**
@ -182,6 +184,25 @@ public final class Settings implements EventListener {
*/
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
*/

View File

@ -1,7 +1,6 @@
package envoy.client.data.commands;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.function.Consumer;
/**
@ -22,6 +21,22 @@ public final class SystemCommandBuilder {
private String description;
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
* @return this {@code SystemCommandBuilder}
@ -125,6 +140,7 @@ public final class SystemCommandBuilder {
/**
* 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.
*
@ -141,4 +157,78 @@ public final class SystemCommandBuilder {
if (reset) reset();
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

@ -12,19 +12,19 @@ import envoy.util.EnvoyLog;
* This class stores all {@link SystemCommand}s used.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>SystemCommandsMap.java</strong><br>
* File: <strong>SystemCommandMap.java</strong><br>
* Created: <strong>17.07.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public final class SystemCommandsMap {
public final class SystemCommandMap {
private final Map<String, SystemCommand> systemCommands = new HashMap<>();
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.
@ -33,7 +33,7 @@ public final class SystemCommandsMap {
* given action
* @param systemCommand the command to add - can be built using
* {@link SystemCommandBuilder}
* @see SystemCommandsMap#isValidKey(String)
* @see SystemCommandMap#isValidKey(String)
* @since Envoy Client v0.2-beta
*/
public void add(String command, SystemCommand systemCommand) {
@ -48,7 +48,7 @@ public final class SystemCommandsMap {
* map).
* <p>
* Usage example:<br>
* {@code SystemCommandsMap systemCommands = new SystemCommandsMap();}<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code Button button = new Button();}
* {@code systemCommands.add("example", text -> button.setText(text.get(0), 1);}<br>
* {@code ....}<br>
@ -132,7 +132,7 @@ public final class SystemCommandsMap {
* map).
* <p>
* Usage example:<br>
* {@code SystemCommandsMap systemCommands = new SystemCommandsMap();}<br>
* {@code SystemCommandMap systemCommands = new SystemCommandMap();}<br>
* {@code Button button = new Button();}<br>
* {@code systemCommands.add("example", (words)-> button.setText(words.get(0), 1);}<br>
* {@code ....}<br>

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 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,45 @@
package envoy.client.helper;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent;
import dev.kske.eventbus.EventBus;
/**
* Contains methods that have a direct impact on the user.
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
public 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() { exit(Settings.getInstance().isHideOnClose(), true); }
/**
* Exits the currently open Envoy application or minimizes it.
* Does not necessarily shutdown VM.
*
* @param minimize whether Envoy should be minimized instead of closed
* @param shutdownVM whether the JVM should shutdown
* @since Envoy Client v0.2-beta
*/
public static void exit(boolean minimize, boolean shutdownVM) {
if (minimize) Context.getInstance().getStage().setIconified(true);
else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
if (shutdownVM) System.exit(0);
}
}
}

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

@ -11,7 +11,8 @@ import javafx.scene.input.*;
import javafx.stage.Stage;
import envoy.client.data.Settings;
import envoy.client.event.*;
import envoy.client.event.ThemeChangeEvent;
import envoy.client.helper.ShutdownHelper;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.*;
@ -108,17 +109,7 @@ public final class SceneContext implements EventListener {
stage.setScene(scene);
// Adding the option to exit Linux-like with "Control" + "Q"
scene.getAccelerators()
.put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN),
() -> {
// Presumably no Settings are loaded in the login scene, hence Envoy is closed
// directly
if (sceneInfo != SceneInfo.LOGIN_SCENE && settings.isHideOnClose()) stage.setIconified(true);
else {
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
System.exit(0);
}
});
scene.getAccelerators().put(new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN), ShutdownHelper::exit);
// 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

View File

@ -11,7 +11,7 @@ import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage;
import envoy.client.data.*;
import envoy.client.event.EnvoyCloseEvent;
import envoy.client.helper.ShutdownHelper;
import envoy.client.net.Client;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene;
@ -21,10 +21,8 @@ import envoy.event.*;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
import dev.kske.eventbus.EventBus;
/**
* Handles application startup and shutdown.
* Handles application startup.
* <p>
* Project: <strong>envoy-client</strong><br>
* File: <strong>Startup.java</strong><br>
@ -212,12 +210,7 @@ public final class Startup extends Application {
if (StatusTrayIcon.isSupported()) {
// Configure hide on close
stage.setOnCloseRequest(e -> {
if (Settings.getInstance().isHideOnClose()) {
stage.setIconified(true);
e.consume();
} else EventBus.getInstance().dispatch(new EnvoyCloseEvent());
});
stage.setOnCloseRequest(e -> { ShutdownHelper.exit(); if (Settings.getInstance().isHideOnClose()) e.consume(); });
// Initialize status tray icon
final var trayIcon = new StatusTrayIcon(stage);

View File

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

View File

@ -144,7 +144,7 @@ public final class ChatScene implements EventListener, Restorable {
private final WriteProxy writeProxy = context.getWriteProxy();
private final SceneContext sceneContext = context.getSceneContext();
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 static Image DEFAULT_ATTACHMENT_VIEW_IMAGE = IconUtil.loadIconThemeSensitive("attachment_present", 20);
@ -329,14 +329,15 @@ public final class ChatScene implements EventListener, Restorable {
* @since Envoy Client v0.2-beta
*/
private void initializeSystemCommandsMap() {
final var builder = new SystemCommandBuilder();
final var builder = new SystemCommandBuilder(messageTextAreaCommands);
// Do A Barrel roll initialization
final var random = new Random();
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))
.setDescription("See for yourself :)")
.setNumberOfArguments(2);
messageTextAreaCommands.add("DABR", builder.build());
.setNumberOfArguments(2)
.build("dabr");
}
@Override

View File

@ -8,6 +8,7 @@ import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import envoy.client.event.*;
import envoy.client.helper.AlertHelper;
import envoy.client.ui.listcell.*;
import envoy.data.User;
import envoy.event.ElementOperation;
@ -42,9 +43,10 @@ public class ContactSearchTab implements EventListener {
@FXML
private ListView<User> userList;
private Alert alert = new Alert(AlertType.CONFIRMATION);
private User currentlySelectedUser;
private final Alert alert = new Alert(AlertType.CONFIRMATION);
private static final EventBus eventBus = EventBus.getInstance();
private static final Logger logger = EnvoyLog.getLogger(ChatScene.class);
@ -52,6 +54,7 @@ public class ContactSearchTab implements EventListener {
private void initialize() {
eventBus.registerListener(this);
userList.setCellFactory(new ListCellFactory<>(ContactControl::new));
alert.setTitle("Add User?");
}
@Event
@ -102,17 +105,24 @@ public class ContactSearchTab implements EventListener {
private void userListClicked() {
final var user = userList.getSelectionModel().getSelectedItem();
if (user != null) {
currentlySelectedUser = user;
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
// Sends the event to the server
eventBus.dispatch(new SendEvent(event));
// Removes the chosen user and updates the UI
userList.getItems().remove(currentlySelectedUser);
eventBus.dispatch(event);
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
currentlySelectedUser = user;
alert.setContentText("Add user " + currentlySelectedUser.getName() + " to your contacts?");
AlertHelper.confirmAction(alert, this::addAsContact);
}
}
private void addAsContact() {
// Sends the event to the server
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
eventBus.dispatch(new SendEvent(event));
// Removes the chosen user and updates the UI
userList.getItems().remove(currentlySelectedUser);
eventBus.dispatch(event);
logger.log(Level.INFO, "Added user " + currentlySelectedUser);
}
@FXML
private void backButtonClicked() { eventBus.dispatch(new BackEvent()); }
}

View File

@ -3,7 +3,7 @@ package envoy.client.ui.settings;
import javafx.scene.control.*;
import envoy.client.data.SettingsItem;
import envoy.client.event.ThemeChangeEvent;
import envoy.client.event.*;
import envoy.data.User.UserStatus;
import dev.kske.eventbus.EventBus;
@ -28,23 +28,30 @@ public final class GeneralSettingsPane extends SettingsPane {
// TODO: Support other value types
final var settingsItems = settings.getItems();
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);
final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem<Boolean>) settingsItems.get("enterToSend"));
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);
enterToSendCheckbox.setTooltip(enterToSendTooltip);
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>();
combobox.getItems().add("dark");
combobox.getItems().add("light");
combobox.setTooltip(new Tooltip("Determines the current theme Envoy will be displayed in."));
combobox.setValue(settings.getCurrentTheme());
combobox.setOnAction(
e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); });
combobox.setOnAction(e -> { settings.setCurrentTheme(combobox.getValue()); EventBus.getInstance().dispatch(new ThemeChangeEvent()); });
getChildren().add(combobox);
final var statusComboBox = new ComboBox<UserStatus>();