Make LocalDB Thread Safe and Simplify its API #38
@ -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>
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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...)
|
||||||
|
@ -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);
|
||||||
} catch (IOException | EnvoyException e) {
|
} catch (IOException | EnvoyException e) {
|
||||||
@ -95,11 +95,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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,9 +120,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());
|
||||||
|
@ -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)));
|
||||||
@ -210,7 +216,7 @@ public final class ChatScene implements EventListener, Restorable {
|
|||||||
@Event(eventType = BackEvent.class)
|
@Event(eventType = BackEvent.class)
|
||||||
private void onBackEvent() { tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal()); }
|
private void onBackEvent() { tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal()); }
|
||||||
|
|
||||||
@Event
|
@Event(includeSubtypes = true)
|
||||||
private void onMessage(Message message) {
|
private void onMessage(Message message) {
|
||||||
|
|
||||||
// The sender of the message is the recipient of the chat
|
// The sender of the message is the recipient of the chat
|
||||||
@ -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) {
|
||||||
|
@ -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); };
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.kske</groupId>
|
<groupId>dev.kske</groupId>
|
||||||
<artifactId>event-bus</artifactId>
|
<artifactId>event-bus</artifactId>
|
||||||
<version>0.0.3</version>
|
<version>0.0.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user