From 9934eefd418bcd738a04869ff981085e779dbe8c Mon Sep 17 00:00:00 2001 From: delvh Date: Fri, 2 Oct 2020 15:23:21 +0200 Subject: [PATCH] Move SystemComandMap From ChatScene to Its Own Component (#74) Move SystemComandMap from ChatScene to its own component. Create message specific commands with their own parser. Fix separators not shown correctly in TextInputContextMenu. Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/74 Reviewed-by: kske Reviewed-by: DieGurke --- client/.classpath | 1 + .../envoy/client/data/commands/Callable.java | 23 +++ .../envoy/client/data/commands/OnCall.java | 30 ---- .../client/data/commands/SystemCommand.java | 35 ++--- .../data/commands/SystemCommandMap.java | 25 ++- .../ui/chatscene/ChatSceneCommands.java | 144 ++++++++++++++++++ .../TextInputContextMenu.java | 8 +- .../client/ui/chatscene/package-info.java | 7 + .../envoy/client/ui/controller/ChatScene.java | 76 ++------- client/src/main/java/module-info.java | 1 + 10 files changed, 218 insertions(+), 132 deletions(-) create mode 100644 client/src/main/java/envoy/client/data/commands/Callable.java delete mode 100644 client/src/main/java/envoy/client/data/commands/OnCall.java create mode 100644 client/src/main/java/envoy/client/ui/chatscene/ChatSceneCommands.java rename client/src/main/java/envoy/client/ui/{control => chatscene}/TextInputContextMenu.java (95%) create mode 100644 client/src/main/java/envoy/client/ui/chatscene/package-info.java 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;