Move SystemComandMap From ChatScene to Its Own Component #74

Merged
delvh merged 5 commits from f/chatscene-commands into develop 2020-10-02 15:23:22 +02:00
8 changed files with 195 additions and 79 deletions
Showing only changes of commit 7468947728 - Show all commits

View File

@ -21,6 +21,7 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
<attribute name="module" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">

View File

@ -8,8 +8,8 @@ import java.util.function.*;
* action and a number of arguments that should be used as input for this * action and a number of arguments that should be used as input for this
* function. * function.
* No {@code SystemCommand} can return anything. * No {@code SystemCommand} can return anything.
* Every {@code SystemCommand} must have as argument type {@code List<String>} so * Every {@code SystemCommand} must have as argument type {@code List<String>}
* that the words following the indicator String can be used as input of the * so that the words following the indicator String can be used as input of the
* function. This approach has one limitation:<br> * function. This approach has one limitation:<br>
* <b>Order matters!</b> Changing the order of arguments will likely result in * <b>Order matters!</b> Changing the order of arguments will likely result in
* unexpected behavior. * unexpected behavior.
@ -115,14 +115,13 @@ public final class SystemCommand implements OnCall {
if (this == obj) return true; if (this == obj) return true;
if (obj == null) return false; if (obj == null) return false;
if (getClass() != obj.getClass()) return false; if (getClass() != obj.getClass()) return false;
final SystemCommand other = (SystemCommand) obj; final var other = (SystemCommand) obj;
return Objects.equals(action, other.action); return Objects.equals(action, other.action);
} }
@Override @Override
public String toString() { public String toString() {
return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", " return "SystemCommand [relevance=" + relevance + ", numberOfArguments=" + numberOfArguments + ", "
+ (action != null ? "action=" + action + ", " : "") + (description != null ? "description=" + description + ", " : "") + (description != null ? "description=" + description + ", " : "") + (defaults != null ? "defaults=" + defaults : "") + "]";
+ (defaults != null ? "defaults=" + defaults : "") + "]";
} }
} }

View File

@ -73,9 +73,14 @@ public final class SystemCommandMap {
* exception: for recommendation purposes. * exception: for recommendation purposes.
*/ */
public String getCommand(String raw) { public String getCommand(String raw) {
final var trimmed = raw.stripLeading(); final var trimmed = raw.stripLeading();
final var index = trimmed.indexOf(' ');
return trimmed.substring(trimmed.charAt(0) == '/' ? 1 : 0, index < 1 ? trimmed.length() : index); // 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 * @since Envoy Client v0.2-beta
*/ */
public boolean isValidKey(String command) { 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, if (!valid) logger.log(Level.WARNING,
"The command \"" + command "The command \"" + command
+ "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters " + "\" is not valid. As it will cause problems in execution, it will not be entered into the map. Only the characters "
@ -152,8 +157,19 @@ public final class SystemCommandMap {
try { try {
systemCommand.getAction().accept(arguments); systemCommand.getAction().accept(arguments);
systemCommand.onCall(); systemCommand.onCall();
} catch (final IndexOutOfBoundsException e) {
logger.log(Level.SEVERE,
String.format(
"System command %s threw an IndexOutOfBoundsException. The most likely cause is a lower number of arguments than expected. It could also be a problem of the action performed: ",
command),
e);
} 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) { } 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(); return value.isPresent();
@ -241,7 +257,7 @@ public final class SystemCommandMap {
final var numberOfArguments = toEvaluate.getNumberOfArguments(); final var numberOfArguments = toEvaluate.getNumberOfArguments();
final List<String> result = new ArrayList<>(); final List<String> 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; String textArg = null;
if (index < textArguments.length) textArg = textArguments[index]; if (index < textArguments.length) textArg = textArguments[index];
// Set the argument at position index to the current argument of the text, if it // Set the argument at position index to the current argument of the text, if it

View File

@ -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<Message> 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<Message> 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<Message> action, Predicate<Message> 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<Message> action, Predicate<Message> 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<Message> action, Predicate<Message> 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; }
}

View File

@ -1,4 +1,4 @@
package envoy.client.ui.control; package envoy.client.ui.chatscene;
import java.util.function.Consumer; 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 deleteMI = new MenuItem("Delete selection");
private final MenuItem clearMI = new MenuItem("Clear"); private final MenuItem clearMI = new MenuItem("Clear");
private final MenuItem selectAllMI = new MenuItem("Select all"); private final MenuItem selectAllMI = new MenuItem("Select all");
private final MenuItem separatorMI = new SeparatorMenuItem();
/** /**
* Creates a new {@code TextInputContextMenu} with an optional action when * 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 // Add all items to the ContextMenu
getItems().add(undoMI); getItems().add(undoMI);
getItems().add(redoMI); getItems().add(redoMI);
getItems().add(new SeparatorMenuItem());
getItems().add(cutMI); getItems().add(cutMI);
getItems().add(copyMI); getItems().add(copyMI);
getItems().add(pasteMI); getItems().add(pasteMI);
getItems().add(separatorMI); getItems().add(new SeparatorMenuItem());
getItems().add(deleteMI); getItems().add(deleteMI);
getItems().add(clearMI); getItems().add(clearMI);
getItems().add(separatorMI); getItems().add(new SeparatorMenuItem());
getItems().add(selectAllMI); getItems().add(selectAllMI);
} }

View File

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

View File

@ -6,7 +6,6 @@ import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.logging.*; import java.util.logging.*;
import javafx.animation.RotateTransition; import javafx.animation.RotateTransition;
@ -26,13 +25,11 @@ import javafx.util.Duration;
import envoy.client.data.*; import envoy.client.data.*;
import envoy.client.data.audio.AudioRecorder; import envoy.client.data.audio.AudioRecorder;
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.chatscene.*;
import envoy.client.ui.control.*; import envoy.client.ui.control.ChatControl;
import envoy.client.ui.listcell.*; import envoy.client.ui.listcell.*;
import envoy.client.util.*; import envoy.client.util.*;
import envoy.data.*; import envoy.data.*;
@ -138,14 +135,14 @@ public final class ChatScene implements EventListener, Restorable {
private Attachment pendingAttachment; private Attachment pendingAttachment;
private boolean postingPermanentlyDisabled; private boolean postingPermanentlyDisabled;
private boolean isCustomAttachmentImage; private boolean isCustomAttachmentImage;
private ChatSceneCommands commands;
private final LocalDB localDB = context.getLocalDB(); private final LocalDB localDB = context.getLocalDB();
private final Client client = context.getClient(); private final Client client = context.getClient();
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 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);
@ -164,6 +161,7 @@ public final class ChatScene implements EventListener, Restorable {
@FXML @FXML
private void initialize() { private void initialize() {
eventBus.registerListener(this); eventBus.registerListener(this);
commands = new ChatSceneCommands(messageList, this);
// Initialize message and user rendering // Initialize message and user rendering
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
@ -192,8 +190,6 @@ public final class ChatScene implements EventListener, Restorable {
chatList.setItems(chats = new FilteredList<>(localDB.getChats())); chatList.setItems(chats = new FilteredList<>(localDB.getChats()));
contactLabel.setText(localDB.getUser().getName()); contactLabel.setText(localDB.getUser().getName());
initializeSystemCommandsMap();
Platform.runLater(() -> { Platform.runLater(() -> {
final var online = client.isOnline(); final var online = client.isOnline();
// no check will be performed in case it has already been disabled - a negative // 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) @Event(eventType = Logout.class, priority = 200)
private void onLogout() { eventBus.removeListener(this); } 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 @Override
public void onRestore() { updateRemainingCharsLabel(); } 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 * @param animationTime the time in seconds that this animation lasts
* @since Envoy Client v0.1-beta * @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 // Limiting the rotations and duration
rotations = Math.min(rotations, 100000); rotations = Math.min(rotations, 100000);
rotations = Math.max(rotations, 1); rotations = Math.max(rotations, 1);
@ -677,7 +625,7 @@ public final class ChatScene implements EventListener, Restorable {
return; return;
} }
final var text = messageTextArea.getText().strip(); final var text = messageTextArea.getText().strip();
if (!messageTextAreaCommands.executeIfAnyPresent(text)) { if (!commands.getChatSceneCommands().executeIfAnyPresent(text)) {
// Creating the message and its metadata // Creating the message and its metadata
final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) final var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator())
.setText(text); .setText(text);

View File

@ -20,6 +20,7 @@ module envoy.client {
opens envoy.client.ui to javafx.graphics, javafx.fxml, dev.kske.eventbus; 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.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.control to javafx.graphics, javafx.fxml;
opens envoy.client.ui.settings to envoy.client.util; opens envoy.client.ui.settings to envoy.client.util;
opens envoy.client.net to dev.kske.eventbus; opens envoy.client.net to dev.kske.eventbus;