diff --git a/client/.classpath b/client/.classpath
index 4328dab..524f8bb 100644
--- a/client/.classpath
+++ b/client/.classpath
@@ -21,6 +21,7 @@
+
diff --git a/client/src/main/java/envoy/client/data/commands/Callable.java b/client/src/main/java/envoy/client/data/commands/Callable.java
new file mode 100644
index 0000000..cd8c146
--- /dev/null
+++ b/client/src/main/java/envoy/client/data/commands/Callable.java
@@ -0,0 +1,23 @@
+package envoy.client.data.commands;
+
+import java.util.List;
+
+/**
+ * This interface defines an action that should be performed when a system
+ * command gets called.
+ *
+ * @author Leon Hofmeister
+ * @since Envoy Client v0.2-beta
+ */
+public interface Callable {
+
+ /**
+ * Performs the instance specific action when a {@link SystemCommand} has been
+ * called.
+ *
+ * @param arguments the arguments that should be passed to the
+ * {@link SystemCommand}
+ * @since Envoy Client v0.2-beta
+ */
+ void call(List arguments);
+}
diff --git a/client/src/main/java/envoy/client/data/commands/OnCall.java b/client/src/main/java/envoy/client/data/commands/OnCall.java
deleted file mode 100644
index 941b4fa..0000000
--- a/client/src/main/java/envoy/client/data/commands/OnCall.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package envoy.client.data.commands;
-
-import java.util.function.Supplier;
-
-/**
- * This interface defines an action that should be performed when a system
- * command gets called.
- *
- * @author Leon Hofmeister
- * @since Envoy Client v0.2-beta
- */
-public interface OnCall {
-
- /**
- * Performs class specific actions when a {@link SystemCommand} has been called.
- *
- * @since Envoy Client v0.2-beta
- */
- void onCall();
-
- /**
- * Performs actions that can only be performed by classes that are not
- * {@link SystemCommand}s when a SystemCommand has been called.
- *
- * @param consumer the action to perform when this {@link SystemCommand} has
- * been called
- * @since Envoy Client v0.2-beta
- */
- void onCall(Supplier consumer);
-}
diff --git a/client/src/main/java/envoy/client/data/commands/SystemCommand.java b/client/src/main/java/envoy/client/data/commands/SystemCommand.java
index a09bd69..7a788dd 100644
--- a/client/src/main/java/envoy/client/data/commands/SystemCommand.java
+++ b/client/src/main/java/envoy/client/data/commands/SystemCommand.java
@@ -1,15 +1,15 @@
package envoy.client.data.commands;
import java.util.*;
-import java.util.function.*;
+import java.util.function.Consumer;
/**
* This class is the base class of all {@code SystemCommands} and contains an
* action and a number of arguments that should be used as input for this
* function.
* No {@code SystemCommand} can return anything.
- * Every {@code SystemCommand} must have as argument type {@code List} so
- * that the words following the indicator String can be used as input of the
+ * Every {@code SystemCommand} must have as argument type {@code List}
+ * so that the words following the indicator String can be used as input of the
* function. This approach has one limitation:
* Order matters! Changing the order of arguments will likely result in
* unexpected behavior.
@@ -17,7 +17,7 @@ import java.util.function.*;
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
*/
-public final class SystemCommand implements OnCall {
+public final class SystemCommand implements Callable {
protected int relevance;
@@ -55,12 +55,6 @@ public final class SystemCommand implements OnCall {
this.description = description;
}
- /**
- * @return the action that should be performed
- * @since Envoy Client v0.2-beta
- */
- public Consumer> getAction() { return action; }
-
/**
* @return the argument count of the command
* @since Envoy Client v0.2-beta
@@ -85,20 +79,10 @@ public final class SystemCommand implements OnCall {
*/
public void setRelevance(int relevance) { this.relevance = relevance; }
- /**
- * Increments the relevance of this {@code SystemCommand}.
- */
@Override
- public void onCall() { relevance++; }
-
- /**
- * Increments the relevance of this {@code SystemCommand} and executes the
- * supplier.
- */
- @Override
- public void onCall(Supplier consumer) {
- onCall();
- consumer.get();
+ public void call(List arguments) {
+ action.accept(arguments);
+ ++relevance;
}
/**
@@ -115,14 +99,13 @@ public final class SystemCommand implements OnCall {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
- final SystemCommand other = (SystemCommand) obj;
+ final var other = (SystemCommand) obj;
return Objects.equals(action, other.action);
}
@Override
public String toString() {
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", "
- + (action != null ? "action=" + action + ", " : "") + (description != null ? "description=" + description + ", " : "")
- + (defaults != null ? "defaults=" + defaults : "") + "]";
+ + (description != null ? "description=" + description + ", " : "") + (defaults != null ? "defaults=" + defaults : "") + "]";
}
}
diff --git a/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java b/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java
index 915807b..ddb2d5e 100644
--- a/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java
+++ b/client/src/main/java/envoy/client/data/commands/SystemCommandMap.java
@@ -73,9 +73,14 @@ public final class SystemCommandMap {
* exception: for recommendation purposes.
*/
public String getCommand(String raw) {
- final var trimmed = raw.stripLeading();
- final var index = trimmed.indexOf(' ');
- return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index);
+ final var trimmed = raw.stripLeading();
+
+ // Entering only a slash should not throw an error
+ if (trimmed.length() == 1 && trimmed.charAt(0) == '/') return "";
+ else {
+ final var index = trimmed.indexOf(' ');
+ return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index);
+ }
}
/**
@@ -92,7 +97,7 @@ public final class SystemCommandMap {
* @since Envoy Client v0.2-beta
*/
public boolean isValidKey(String command) {
- final boolean valid = commandPattern.matcher(command).matches();
+ final var valid = commandPattern.matcher(command).matches();
if (!valid) logger.log(Level.WARNING,
"The command \"" + command
+ "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters "
@@ -150,10 +155,14 @@ public final class SystemCommandMap {
final var arguments = extractArguments(input, systemCommand);
// Executing the function
try {
- systemCommand.getAction().accept(arguments);
- systemCommand.onCall();
+ systemCommand.call(arguments);
+ } catch (final NumberFormatException e) {
+ logger.log(Level.INFO,
+ String.format(
+ "System command %s could not be performed correctly because the user is a dumbass and could not write a parseable number.",
+ command));
} catch (final Exception e) {
- logger.log(Level.WARNING, "The system command " + command + " threw an exception: ", e);
+ logger.log(Level.WARNING, "System command " + command + " threw an exception: ", e);
}
});
return value.isPresent();
@@ -241,7 +250,7 @@ public final class SystemCommandMap {
final var numberOfArguments = toEvaluate.getNumberOfArguments();
final List result = new ArrayList<>();
- if (toEvaluate.getNumberOfArguments() > 0) for (int index = 0; index < numberOfArguments; index++) {
+ if (toEvaluate.getNumberOfArguments() > 0) for (var index = 0; index < numberOfArguments; index++) {
String textArg = null;
if (index < textArguments.length) textArg = textArguments[index];
// Set the argument at position index to the current argument of the text, if it
diff --git a/client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java b/client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java
new file mode 100644
index 0000000..850d8bb
--- /dev/null
+++ b/client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java
@@ -0,0 +1,144 @@
+package envoy.client.ui.chatscene;
+
+import java.util.Random;
+import java.util.function.*;
+import java.util.logging.Level;
+
+import javafx.scene.control.ListView;
+import javafx.scene.control.skin.VirtualFlow;
+
+import envoy.client.data.Context;
+import envoy.client.data.commands.*;
+import envoy.client.helper.ShutdownHelper;
+import envoy.client.ui.SceneContext.SceneInfo;
+import envoy.client.ui.controller.ChatScene;
+import envoy.client.util.MessageUtil;
+import envoy.data.Message;
+import envoy.util.EnvoyLog;
+
+/**
+ * Contains all {@link SystemCommand}s used for
+ * {@link envoy.client.ui.controller.ChatScene}.
+ *
+ * @author Leon Hofmeister
+ * @since Envoy Client v0.3-beta
+ */
+public final class ChatSceneCommands {
+
+ private final ListView messageList;
+ private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
+ private final SystemCommandBuilder builder = new SystemCommandBuilder(messageTextAreaCommands);
+
+ private static final String messageDependantCommandDescription = " the given message. Use s/S to use the selected message. Otherwise expects a number relative to the uppermost completely visible message.";
+
+ /**
+ *
+ * @param messageList the message list to use for some commands
+ * @param chatScene the instance of {@code ChatScene} that uses this object
+ * @since Envoy Client v0.3-beta
+ */
+ public ChatSceneCommands(ListView messageList, ChatScene chatScene) {
+ this.messageList = messageList;
+
+ // Do A Barrel roll initialization
+ final var random = new Random();
+ builder.setAction(text -> chatScene.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)
+ .build("dabr");
+
+ // Logout initialization
+ builder.setAction(text -> ShutdownHelper.logout()).setDescription("Logs you out.").buildNoArg("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 -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE))
+ .setDescription("Opens the settings screen")
+ .buildNoArg("settings");
+
+ // Selection of a new message initialization
+ messageDependantAction("s",
+ m -> { messageList.getSelectionModel().clearSelection(); messageList.getSelectionModel().select(m); },
+ m -> true,
+ "Selects");
+
+ // Copy text of selection initialization
+ messageDependantAction("cp", MessageUtil::copyMessageText, m -> !m.getText().isEmpty(), "Copies the text of");
+
+ // Delete selection initialization
+ messageDependantAction("del", MessageUtil::deleteMessage, m -> true, "Deletes");
+
+ // Save attachment of selection initialization
+ messageDependantAction("save-att", MessageUtil::saveAttachment, Message::hasAttachment, "Saves the attachment of");
+ }
+
+ private void messageDependantAction(String command, Consumer action, Predicate additionalCheck, String description) {
+ builder.setAction(text -> {
+ final var positionalArgument = text.get(0).toLowerCase();
+
+ // the currently selected message was requested
+ if (positionalArgument.startsWith("s")) {
+ final var relativeString = positionalArgument.length() == 1 ? "" : positionalArgument.substring(1);
+
+ // Only s has been used as input
+ if (positionalArgument.length() == 1) {
+ final var selectedMessage = messageList.getSelectionModel().getSelectedItem();
+ if (selectedMessage != null && additionalCheck.test(selectedMessage)) action.accept(selectedMessage);
+ return;
+
+ // Either s++ or s-- has been requested
+ } else if (relativeString.equals("++") || relativeString.equals("--")) selectionNeighbor(action, additionalCheck, positionalArgument);
+
+ // A message relative to the currently selected message should be used (i.e.
+ // s+4)
+ else useRelativeMessage(command, action, additionalCheck, relativeString, true);
+
+ // Either ++s or --s has been requested
+ } else if (positionalArgument.equals("--s") || positionalArgument.equals("++s"))
+ selectionNeighbor(action, additionalCheck, positionalArgument);
+
+ // Just a number is expected: ((+)4)
+ else useRelativeMessage(command, action, additionalCheck, positionalArgument, false);
+ }).setDefaults("s").setNumberOfArguments(1).setDescription(description.concat(messageDependantCommandDescription)).build(command);
+ }
+
+ private void selectionNeighbor(Consumer action, Predicate additionalCheck, final String positionalArgument) {
+ final var wantedIndex = messageList.getSelectionModel().getSelectedIndex() + (positionalArgument.contains("+") ? 1 : -1);
+ messageList.getSelectionModel().clearAndSelect(wantedIndex);
+ final var selectedMessage = messageList.getItems().get(wantedIndex);
+ if (selectedMessage != null && additionalCheck.test(selectedMessage)) action.accept(selectedMessage);
+ }
+
+ private void useRelativeMessage(String command, Consumer action, Predicate additionalCheck, final String positionalArgument,
+ boolean useSelectedMessage) throws NumberFormatException {
+ final var stripPlus = positionalArgument.startsWith("+") ? positionalArgument.substring(1) : positionalArgument;
+ final var incDec = Integer.valueOf(stripPlus);
+ try {
+
+ // The currently selected message is the base message
+ if (useSelectedMessage) {
+ final var messageToUse = messageList.getItems().get(messageList.getSelectionModel().getSelectedIndex() + incDec);
+ if (messageToUse != null && additionalCheck.test(messageToUse)) action.accept(messageToUse);
+
+ // The currently upmost completely visible message is the base message
+ } else {
+ final var messageToUse = messageList.getItems()
+ .get(((VirtualFlow>) messageList.lookup(".virtual-flow")).getFirstVisibleCell().getIndex() + 1 + incDec);
+ if (messageToUse != null && additionalCheck.test(messageToUse)) action.accept(messageToUse);
+ }
+ } catch (final IndexOutOfBoundsException e) {
+ EnvoyLog.getLogger(ChatSceneCommands.class)
+ .log(Level.INFO, " A non-existing message was requested by the user for System command " + command);
+ }
+ }
+
+ /**
+ * @return the map used by this {@code ChatSceneCommands}
+ * @since Envoy Client v0.3-beta
+ */
+ public SystemCommandMap getChatSceneCommands() { return messageTextAreaCommands; }
+}
diff --git a/client/src/main/java/envoy/client/ui/control/TextInputContextMenu.java b/client/src/main/java/envoy/client/ui/chatscene/TextInputContextMenu.java
similarity index 95%
rename from client/src/main/java/envoy/client/ui/control/TextInputContextMenu.java
rename to client/src/main/java/envoy/client/ui/chatscene/TextInputContextMenu.java
index bafed83..e11b6ca 100644
--- a/client/src/main/java/envoy/client/ui/control/TextInputContextMenu.java
+++ b/client/src/main/java/envoy/client/ui/chatscene/TextInputContextMenu.java
@@ -1,4 +1,4 @@
-package envoy.client.ui.control;
+package envoy.client.ui.chatscene;
import java.util.function.Consumer;
@@ -38,7 +38,6 @@ public class TextInputContextMenu extends ContextMenu {
private final MenuItem deleteMI = new MenuItem("Delete selection");
private final MenuItem clearMI = new MenuItem("Clear");
private final MenuItem selectAllMI = new MenuItem("Select all");
- private final MenuItem separatorMI = new SeparatorMenuItem();
/**
* Creates a new {@code TextInputContextMenu} with an optional action when
@@ -90,13 +89,14 @@ public class TextInputContextMenu extends ContextMenu {
// Add all items to the ContextMenu
getItems().add(undoMI);
getItems().add(redoMI);
+ getItems().add(new SeparatorMenuItem());
getItems().add(cutMI);
getItems().add(copyMI);
getItems().add(pasteMI);
- getItems().add(separatorMI);
+ getItems().add(new SeparatorMenuItem());
getItems().add(deleteMI);
getItems().add(clearMI);
- getItems().add(separatorMI);
+ getItems().add(new SeparatorMenuItem());
getItems().add(selectAllMI);
}
diff --git a/client/src/main/java/envoy/client/ui/chatscene/package-info.java b/client/src/main/java/envoy/client/ui/chatscene/package-info.java
new file mode 100644
index 0000000..12938c6
--- /dev/null
+++ b/client/src/main/java/envoy/client/ui/chatscene/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Contains classes that influence the appearance and behavior of ChatScene.
+ *
+ * @author Leon Hofmeister
+ * @since Envoy Client v0.3-beta
+ */
+package envoy.client.ui.chatscene;
diff --git a/client/src/main/java/envoy/client/ui/controller/ChatScene.java b/client/src/main/java/envoy/client/ui/controller/ChatScene.java
index a4ef479..8ec0246 100644
--- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java
+++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java
@@ -6,7 +6,6 @@ import java.io.*;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
-import java.util.Random;
import java.util.logging.*;
import javafx.animation.RotateTransition;
@@ -26,13 +25,11 @@ import javafx.util.Duration;
import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder;
-import envoy.client.data.commands.*;
import envoy.client.event.*;
-import envoy.client.helper.ShutdownHelper;
import envoy.client.net.*;
import envoy.client.ui.*;
-import envoy.client.ui.SceneContext.SceneInfo;
-import envoy.client.ui.control.*;
+import envoy.client.ui.chatscene.*;
+import envoy.client.ui.control.ChatControl;
import envoy.client.ui.listcell.*;
import envoy.client.util.*;
import envoy.data.*;
@@ -138,14 +135,14 @@ public final class ChatScene implements EventListener, Restorable {
private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled;
private boolean isCustomAttachmentImage;
+ private ChatSceneCommands commands;
- private final LocalDB localDB = context.getLocalDB();
- private final Client client = context.getClient();
- private final WriteProxy writeProxy = context.getWriteProxy();
- private final SceneContext sceneContext = context.getSceneContext();
- private final AudioRecorder recorder = new AudioRecorder();
- private final SystemCommandMap messageTextAreaCommands = new SystemCommandMap();
- private final Tooltip onlyIfOnlineTooltip = new Tooltip("You need to be online to do this");
+ private final LocalDB localDB = context.getLocalDB();
+ private final Client client = context.getClient();
+ private final WriteProxy writeProxy = context.getWriteProxy();
+ private final SceneContext sceneContext = context.getSceneContext();
+ private final AudioRecorder recorder = new AudioRecorder();
+ 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);
@@ -164,6 +161,7 @@ public final class ChatScene implements EventListener, Restorable {
@FXML
private void initialize() {
eventBus.registerListener(this);
+ commands = new ChatSceneCommands(messageList, this);
// Initialize message and user rendering
messageList.setCellFactory(MessageListCell::new);
@@ -192,8 +190,6 @@ public final class ChatScene implements EventListener, Restorable {
chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
contactLabel.setText(localDB.getUser().getName());
- initializeSystemCommandsMap();
-
Platform.runLater(() -> {
final var online = client.isOnline();
// no check will be performed in case it has already been disabled - a negative
@@ -308,54 +304,6 @@ public final class ChatScene implements EventListener, Restorable {
@Event(eventType = Logout.class, priority = 200)
private void onLogout() { eventBus.removeListener(this); }
- /**
- * Initializes all {@code SystemCommands} used in {@code ChatScene}.
- *
- * @since Envoy Client v0.2-beta
- */
- private void initializeSystemCommandsMap() {
- 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)
- .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");
-
- // Copy text of selection initialization
- builder.setAction(text -> {
- final var selectedMessage = messageList.getSelectionModel().getSelectedItem();
- if (selectedMessage != null) MessageUtil.copyMessageText(selectedMessage);
- }).setNumberOfArguments(0).setDescription("Copies the text of the currently selected message").build("cp-s");
-
- // Delete selection initialization
- builder.setAction(text -> {
- final var selectedMessage = messageList.getSelectionModel().getSelectedItem();
- if (selectedMessage != null) MessageUtil.deleteMessage(selectedMessage);
- }).setNumberOfArguments(0).setDescription("Deletes the currently selected message").build("del-s");
-
- // Save attachment of selection initialization
- builder.setAction(text -> {
- final var selectedMessage = messageList.getSelectionModel().getSelectedItem();
- if (selectedMessage != null && selectedMessage.hasAttachment()) MessageUtil.saveAttachment(selectedMessage);
- }).setNumberOfArguments(0).setDescription("Copies the text of the currently selected message").build("save-a-s");
- }
-
@Override
public void onRestore() { updateRemainingCharsLabel(); }
@@ -530,7 +478,7 @@ public final class ChatScene implements EventListener, Restorable {
* @param animationTime the time in seconds that this animation lasts
* @since Envoy Client v0.1-beta
*/
- private void doABarrelRoll(int rotations, double animationTime) {
+ public void doABarrelRoll(int rotations, double animationTime) {
// Limiting the rotations and duration
rotations = Math.min(rotations, 100000);
rotations = Math.max(rotations, 1);
@@ -677,7 +625,7 @@ public final class ChatScene implements EventListener, Restorable {
return;
}
final var text = messageTextArea.getText().strip();
- if (!messageTextAreaCommands.executeIfAnyPresent(text)) {
+ if (!commands.getChatSceneCommands().executeIfAnyPresent(text)) {
// Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text);
diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java
index e0f86ba..d894c26 100644
--- a/client/src/main/java/module-info.java
+++ b/client/src/main/java/module-info.java
@@ -20,6 +20,7 @@ module envoy.client {
opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus;
opens envoy.client.ui.controller to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus;
+ opens envoy.client.ui.chatscene to javafx.graphics, javafx.fxml, envoy.client.util, dev.kske.eventbus;
opens envoy.client.ui.control to javafx.graphics, javafx.fxml;
opens envoy.client.ui.settings to envoy.client.util;
opens envoy.client.net to dev.kske.eventbus;