Fixed hopefully every bug concerning "enter to send" ability #36

Merged
delvh merged 5 commits from b/message-text-area into develop 2020-09-21 20:21:47 +02:00
7 changed files with 184 additions and 53 deletions

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<projectDescription> <projectDescription>
<name>client</name> <name>client</name>
<comment></comment> <comment></comment>

View File

@ -1,9 +1,8 @@
package envoy.client.data.commands; package envoy.client.data.commands;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.*;
import java.util.logging.Logger;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -198,13 +197,13 @@ public final class SystemCommandsMap {
* are present * are present
* @since Envoy Client v0.2-beta * @since Envoy Client v0.2-beta
*/ */
public void requestRecommendations(String input, Function<Set<String>, Void> action) { public void requestRecommendations(String input, Consumer<Set<String>> action) {
final var partialCommand = getCommand(input); final var partialCommand = getCommand(input);
// Get the expected commands // Get the expected commands
final var recommendations = recommendCommands(partialCommand); final var recommendations = recommendCommands(partialCommand);
if (recommendations.isEmpty()) return; if (recommendations.isEmpty()) return;
// Execute the given action // Execute the given action
else action.apply(recommendations); else action.accept(recommendations);
} }
/** /**

View File

@ -7,6 +7,7 @@ import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.*; import javafx.scene.*;
import javafx.scene.input.*;
import javafx.stage.Stage; import javafx.stage.Stage;
import envoy.client.data.Settings; import envoy.client.data.Settings;
@ -105,6 +106,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"
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 System.exit(0);
});
// 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
// displayed on some OS (...Debian...) // displayed on some OS (...Debian...)

View File

@ -67,7 +67,7 @@ public final class Startup extends Application {
// Initialize the local database // Initialize the local database
try { try {
File localDBDir = new File(config.getHomeDirectory(), config.getLocalDB().getPath()); final var localDBDir = new File(config.getHomeDirectory(), config.getLocalDB().getPath());
logger.info("Initializing LocalDB at " + localDBDir); logger.info("Initializing LocalDB at " + localDBDir);
localDB = new LocalDB(localDBDir); localDB = new LocalDB(localDBDir);
localDB.lock(); localDB.lock();
@ -99,11 +99,9 @@ public final class Startup extends Application {
if (!performHandshake( if (!performHandshake(
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync()))) LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
} else { } else
// Load login scene // Load login scene
sceneContext.load(SceneInfo.LOGIN_SCENE); sceneContext.load(SceneInfo.LOGIN_SCENE);
}
} }
/** /**
@ -126,9 +124,7 @@ public final class Startup extends Application {
loadChatScene(); loadChatScene();
client.initReceiver(localDB, cacheMap); client.initReceiver(localDB, cacheMap);
return true; return true;
} else { } else return false;
return false;
}
} catch (IOException | InterruptedException | TimeoutException e) { } catch (IOException | InterruptedException | TimeoutException e) {
logger.log(Level.INFO, "Could not connect to server. Entering offline mode..."); logger.log(Level.INFO, "Could not connect to server. Entering offline mode...");
return attemptOfflineMode(credentials.getIdentifier()); return attemptOfflineMode(credentials.getIdentifier());

View File

@ -30,6 +30,7 @@ import envoy.client.data.commands.*;
import envoy.client.event.*; import envoy.client.event.*;
import envoy.client.net.*; import envoy.client.net.*;
import envoy.client.ui.*; import envoy.client.ui.*;
import envoy.client.ui.custom.TextInputContextMenu;
import envoy.client.ui.listcell.*; import envoy.client.ui.listcell.*;
import envoy.client.util.ReflectionUtil; import envoy.client.util.ReflectionUtil;
import envoy.data.*; import envoy.data.*;
@ -168,6 +169,11 @@ public final class ChatScene implements EventListener, Restorable {
messageList.setCellFactory(MessageListCell::new); messageList.setCellFactory(MessageListCell::new);
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new)); chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
// JavaFX provides an internal way of populating the context menu of a textarea.
// We, however, need additional functionality.
messageTextArea.setContextMenu(new TextInputContextMenu(messageTextArea, e -> checkKeyCombination(null)));
// Set the icons of buttons and image views
settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); settingsButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE)));
voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE)));
attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE))); attachmentButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("attachment", DEFAULT_ICON_SIZE)));
@ -542,16 +548,23 @@ public final class ChatScene implements EventListener, Restorable {
*/ */
@FXML @FXML
private void checkKeyCombination(KeyEvent e) { private void checkKeyCombination(KeyEvent e) {
// Checks whether the text is too long // Checks whether the text is too long
messageTextUpdated(); messageTextUpdated();
// Sending an IsTyping event if none has been sent for // Sending an IsTyping event if none has been sent for
// IsTyping#millisecondsActive // IsTyping#millisecondsActive
if (currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) { if (currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) {
eventBus.dispatch(new SendEvent(new IsTyping(getChatID(), currentChat.getRecipient().getID()))); eventBus.dispatch(new SendEvent(new IsTyping(getChatID(), currentChat.getRecipient().getID())));
currentChat.lastWritingEventWasNow(); currentChat.lastWritingEventWasNow();
} }
// Automatic sending of messages via (ctrl +) enter
checkPostConditions(e); // KeyPressed will be called before the char has been added to the text, hence
// this is needed for the first char
if (messageTextArea.getText().length() == 1 && e != null) checkPostConditions(e);
// This is needed for the messageTA context menu
else if (e == null) checkPostConditions(false);
} }
/** /**
@ -572,8 +585,25 @@ public final class ChatScene implements EventListener, Restorable {
*/ */
@FXML @FXML
private void checkPostConditions(KeyEvent e) { private void checkPostConditions(KeyEvent e) {
checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER final var enterPressed = e.getCode() == KeyCode.ENTER;
|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()); final var messagePosted = enterPressed ? settings.isEnterToSend() ? !e.isControlDown() : e.isControlDown() : false;
if (messagePosted) {
// Removing an inserted line break if added by pressing enter
final var text = messageTextArea.getText();
final var textPosition = messageTextArea.getCaretPosition() - 1;
if (!e.isControlDown() && !text.isEmpty() && text.charAt(textPosition) == '\n')
messageTextArea.setText(new StringBuilder(text).deleteCharAt(textPosition).toString());
}
// if control is pressed, the enter press is originally invalidated. Here it'll
// be inserted again
else if (enterPressed && e.isControlDown()) {
var caretPosition = messageTextArea.getCaretPosition();
messageTextArea.setText(new StringBuilder(messageTextArea.getText()).insert(caretPosition, '\n').toString());
messageTextArea.positionCaret(++caretPosition);
}
checkPostConditions(messagePosted);
} }
private void checkPostConditions(boolean postMessage) { private void checkPostConditions(boolean postMessage) {

View File

@ -0,0 +1,109 @@
package envoy.client.ui.custom;
import java.util.function.Consumer;
import javafx.event.*;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
/**
* Displays a context menu that offers an additional option when one of
* its menu items has been clicked.
* <p>
* Current options are:
* <ul>
* <li>undo</li>
* <li>redo</li>
* <li>cut</li>
* <li>copy</li>
* <li>paste</li>
* <li>delete</li>
* <li>clear</li>
* <li>Select all</li>
* </ul>
* <p>
* Project: <strong>client</strong><br>
* File: <strong>TextInputContextMenu.java</strong><br>
* Created: <strong>20.09.2020</strong><br>
*
* @author Leon Hofmeister
* @since Envoy Client v0.2-beta
* @apiNote please refrain from using
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already
* used by this component
*/
public class TextInputContextMenu extends ContextMenu {
private final MenuItem undoMI = new MenuItem("Undo");
private final MenuItem redoMI = new MenuItem("Redo");
private final MenuItem cutMI = new MenuItem("Cut");
private final MenuItem copyMI = new MenuItem("Copy");
private final MenuItem pasteMI = new MenuItem("Paste");
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
* this menu was clicked. Currently shows:
* <ul>
* <li>undo</li>
* <li>redo</li>
* <li>cut</li>
* <li>copy</li>
* <li>paste</li>
* <li>delete</li>
* <li>clear</li>
* <li>Select all</li>
* </ul>
*
* @param control the text input component to display this
* {@code ContextMenu}
* @param menuItemClicked the second action to perform when a menu item of this
* context menu has been clicked
* @since Envoy Client v0.2-beta
* @apiNote please refrain from using
* {@link ContextMenu#setOnShowing(EventHandler)} as this is already
* used by this component
*/
public TextInputContextMenu(TextInputControl control, Consumer<ActionEvent> menuItemClicked) {
// Define the actions when clicked
undoMI.setOnAction(addAction(e -> control.undo(), menuItemClicked));
redoMI.setOnAction(addAction(e -> control.redo(), menuItemClicked));
cutMI.setOnAction(addAction(e -> control.cut(), menuItemClicked));
copyMI.setOnAction(addAction(e -> control.copy(), menuItemClicked));
pasteMI.setOnAction(addAction(e -> control.paste(), menuItemClicked));
deleteMI.setOnAction(addAction(e -> control.replaceSelection(""), menuItemClicked));
clearMI.setOnAction(addAction(e -> control.setText(""), menuItemClicked));
selectAllMI.setOnAction(addAction(e -> control.selectAll(), menuItemClicked));
// Define the times it will be disabled
undoMI.disableProperty().bind(control.undoableProperty().not());
redoMI.disableProperty().bind(control.redoableProperty().not());
cutMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
copyMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
deleteMI.disableProperty().bind(control.selectedTextProperty().isEmpty());
clearMI.disableProperty().bind(control.textProperty().isEmpty());
setOnShowing(e -> pasteMI.setDisable(!Clipboard.getSystemClipboard().hasString()));
selectAllMI.getProperties().put("refreshMenu", Boolean.TRUE);
// Add all items to the ContextMenu
getItems().add(undoMI);
getItems().add(redoMI);
getItems().add(cutMI);
getItems().add(copyMI);
getItems().add(pasteMI);
getItems().add(separatorMI);
getItems().add(deleteMI);
getItems().add(clearMI);
getItems().add(separatorMI);
getItems().add(selectAllMI);
}
private EventHandler<ActionEvent> addAction(Consumer<ActionEvent> originalAction, Consumer<ActionEvent> additionalAction) {
return e -> { originalAction.accept(e); additionalAction.accept(e); };
}
}

View File

@ -1,8 +1,7 @@
package envoy.data; package envoy.data;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.*;
import java.util.Map;
import envoy.data.Message.MessageStatus; import envoy.data.Message.MessageStatus;
@ -80,21 +79,15 @@ public final class MessageBuilder {
* Creates an instance of {@link Message} with the previously supplied values. * Creates an instance of {@link Message} with the previously supplied values.
* If a mandatory value is not set, a default value will be used instead:<br> * If a mandatory value is not set, a default value will be used instead:<br>
* <br> * <br>
* <table border="1"> * {@code date}
* <tr> * {@code Instant.now()} and {@code null} for {@code receivedDate} and
* <td>{@code date}</td> * {@code readDate}
* <td>{@code Instant.now()} and {@code null} for {@code receivedDate} and * <br>
* {@code readDate}</td> * {@code text}
* <tr> * {@code ""}
* <tr> * <br>
* <td>{@code text}</td> * {@code status}
* <td>{@code ""}</td> * {@code MessageStatus.WAITING}
* <tr>
* <tr>
* <td>{@code status}</td>
* <td>{@code MessageStatus.WAITING}</td>
* <tr>
* </table>
* *
* @return a new instance of {@link Message} * @return a new instance of {@link Message}
* @since Envoy Common v0.2-alpha * @since Envoy Common v0.2-alpha
@ -111,16 +104,12 @@ public final class MessageBuilder {
* If a mandatory value is not set, a default value will be used * If a mandatory value is not set, a default value will be used
* instead:<br> * instead:<br>
* <br> * <br>
* <table border="1"> * {@code time stamp}
* <tr> * {@code Instant.now()}
* <td>{@code time stamp}</td> * <br>
* <td>{@code Instant.now()}</td> * {@code text}
* <tr> * {@code ""}
* <tr> * <br>
* <td>{@code text}</td>
* <td>{@code ""}</td>
* <tr>
* </table>
* *
* @param group the {@link Group} that is used to fill the map of member * @param group the {@link Group} that is used to fill the map of member
* statuses * statuses
@ -138,16 +127,11 @@ public final class MessageBuilder {
* values. If a mandatory value is not set, a default value will be used * values. If a mandatory value is not set, a default value will be used
* instead:<br> * instead:<br>
* <br> * <br>
* <table border="1"> * {@code time stamp}
* <tr> * {@code Instant.now()}
* <td>{@code time stamp}</td> * <br>
* <td>{@code Instant.now()}</td> * {@code text}
* <tr> * {@code ""}
* <tr>
* <td>{@code text}</td>
* <td>{@code ""}</td>
* <tr>
* </table>
* *
* @param group the {@link Group} that is used to fill the map of * @param group the {@link Group} that is used to fill the map of
* member statuses * member statuses