Make LocalDB Thread Safe and Simplify its API #38
| @@ -1,4 +1,5 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!DOCTYPE xml> | ||||
| <projectDescription> | ||||
| 	<name>client</name> | ||||
| 	<comment></comment> | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| package envoy.client.data.commands; | ||||
|  | ||||
| import java.util.*; | ||||
| import java.util.function.Function; | ||||
| import java.util.logging.Level; | ||||
| import java.util.logging.Logger; | ||||
| import java.util.function.Consumer; | ||||
| import java.util.logging.*; | ||||
| import java.util.regex.Pattern; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| @@ -198,13 +197,13 @@ public final class SystemCommandsMap { | ||||
| 	 *               are present | ||||
| 	 * @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); | ||||
| 		// Get the expected commands | ||||
| 		final var recommendations = recommendCommands(partialCommand); | ||||
| 		if (recommendations.isEmpty()) return; | ||||
| 		// 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.fxml.FXMLLoader; | ||||
| import javafx.scene.*; | ||||
| import javafx.scene.input.*; | ||||
| import javafx.stage.Stage; | ||||
|  | ||||
| import envoy.client.data.Settings; | ||||
| @@ -105,6 +106,17 @@ public final class SceneContext implements EventListener { | ||||
|  | ||||
| 			sceneStack.push(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 | ||||
| 			// As strange as it seems, this is needed as otherwise the LoginScene won't be | ||||
| 			// displayed on some OS (...Debian...) | ||||
|   | ||||
| @@ -67,7 +67,7 @@ public final class Startup extends Application { | ||||
|  | ||||
| 		// Initialize the local database | ||||
| 		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); | ||||
| 			localDB = new LocalDB(localDBDir); | ||||
| 		} catch (IOException | EnvoyException e) { | ||||
| @@ -95,11 +95,9 @@ public final class Startup extends Application { | ||||
| 			if (!performHandshake( | ||||
| 					LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync()))) | ||||
| 				sceneContext.load(SceneInfo.LOGIN_SCENE); | ||||
| 		} else { | ||||
|  | ||||
| 		} else  | ||||
| 			// Load login scene | ||||
| 			sceneContext.load(SceneInfo.LOGIN_SCENE); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| @@ -122,9 +120,7 @@ public final class Startup extends Application { | ||||
| 				loadChatScene(); | ||||
| 				client.initReceiver(localDB, cacheMap); | ||||
| 				return true; | ||||
| 			} else { | ||||
| 				return false; | ||||
| 			} | ||||
| 			} else return false; | ||||
| 		} catch (IOException | InterruptedException | TimeoutException e) { | ||||
| 			logger.log(Level.INFO, "Could not connect to server. Entering offline mode..."); | ||||
| 			return attemptOfflineMode(credentials.getIdentifier()); | ||||
|   | ||||
| @@ -30,6 +30,7 @@ import envoy.client.data.commands.*; | ||||
| import envoy.client.event.*; | ||||
| import envoy.client.net.*; | ||||
| import envoy.client.ui.*; | ||||
| import envoy.client.ui.custom.TextInputContextMenu; | ||||
| import envoy.client.ui.listcell.*; | ||||
| import envoy.client.util.ReflectionUtil; | ||||
| import envoy.data.*; | ||||
| @@ -168,6 +169,11 @@ public final class ChatScene implements EventListener, Restorable { | ||||
| 		messageList.setCellFactory(MessageListCell::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))); | ||||
| 		voiceButton.setGraphic(new ImageView(IconUtil.loadIconThemeSensitive("microphone", 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) | ||||
| 	private void onBackEvent() { tabPane.getSelectionModel().select(Tabs.CONTACT_LIST.ordinal()); } | ||||
|  | ||||
| 	@Event | ||||
| 	@Event(includeSubtypes = true) | ||||
| 	private void onMessage(Message message) { | ||||
|  | ||||
| 		// The sender of the message is the recipient of the chat | ||||
| @@ -542,16 +548,23 @@ public final class ChatScene implements EventListener, Restorable { | ||||
| 	 */ | ||||
| 	@FXML | ||||
| 	private void checkKeyCombination(KeyEvent e) { | ||||
|  | ||||
| 		// Checks whether the text is too long | ||||
| 		messageTextUpdated(); | ||||
|  | ||||
| 		// Sending an IsTyping event if none has been sent for | ||||
| 		// IsTyping#millisecondsActive | ||||
| 		if (currentChat.getLastWritingEvent() + IsTyping.millisecondsActive <= System.currentTimeMillis()) { | ||||
| 			eventBus.dispatch(new SendEvent(new IsTyping(getChatID(), currentChat.getRecipient().getID()))); | ||||
| 			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 | ||||
| 	private void checkPostConditions(KeyEvent e) { | ||||
| 		checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER | ||||
| 				|| !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()); | ||||
| 		final var	enterPressed	= e.getCode() == KeyCode.ENTER; | ||||
| 		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) { | ||||
|   | ||||
| @@ -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> | ||||
| 			<groupId>dev.kske</groupId> | ||||
| 			<artifactId>event-bus</artifactId> | ||||
| 			<version>0.0.3</version> | ||||
| 			<version>0.0.4</version> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>org.junit.jupiter</groupId> | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| package envoy.data; | ||||
|  | ||||
| import java.time.Instant; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.*; | ||||
|  | ||||
| import envoy.data.Message.MessageStatus; | ||||
|  | ||||
| @@ -80,21 +79,15 @@ public final class MessageBuilder { | ||||
| 	 * 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> | ||||
| 	 * <br> | ||||
| 	 * <table border="1"> | ||||
| 	 * <tr> | ||||
| 	 * <td>{@code date}</td> | ||||
| 	 * <td>{@code Instant.now()} and {@code null} for {@code receivedDate} and | ||||
| 	 * {@code readDate}</td> | ||||
| 	 * <tr> | ||||
| 	 * <tr> | ||||
| 	 * <td>{@code text}</td> | ||||
| 	 * <td>{@code ""}</td> | ||||
| 	 * <tr> | ||||
| 	 * <tr> | ||||
| 	 * <td>{@code status}</td> | ||||
| 	 * <td>{@code MessageStatus.WAITING}</td> | ||||
| 	 * <tr> | ||||
| 	 * </table> | ||||
| 	 * {@code date} | ||||
| 	 * {@code Instant.now()} and {@code null} for {@code receivedDate} and | ||||
| 	 * {@code readDate} | ||||
| 	 * <br> | ||||
| 	 * {@code text} | ||||
| 	 * {@code ""} | ||||
| 	 * <br> | ||||
| 	 * {@code status} | ||||
| 	 * {@code MessageStatus.WAITING} | ||||
| 	 * | ||||
| 	 * @return a new instance of {@link Message} | ||||
| 	 * @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 | ||||
| 	 * instead:<br> | ||||
| 	 * <br> | ||||
| 	 * <table border="1"> | ||||
| 	 * <tr> | ||||
| 	 * <td>{@code time stamp}</td> | ||||
| 	 * <td>{@code Instant.now()}</td> | ||||
| 	 * <tr> | ||||
| 	 * <tr> | ||||
| 	 * <td>{@code text}</td> | ||||
| 	 * <td>{@code ""}</td> | ||||
| 	 * <tr> | ||||
| 	 * </table> | ||||
| 	 * {@code time stamp} | ||||
| 	 * {@code Instant.now()} | ||||
| 	 * <br> | ||||
| 	 * {@code text} | ||||
| 	 * {@code ""} | ||||
| 	 * <br> | ||||
| 	 * | ||||
| 	 * @param group the {@link Group} that is used to fill the map of member | ||||
| 	 *              statuses | ||||
| @@ -138,16 +127,11 @@ public final class MessageBuilder { | ||||
| 	 * values. If a mandatory value is not set, a default value will be used | ||||
| 	 * instead:<br> | ||||
| 	 * <br> | ||||
| 	 * <table border="1"> | ||||
| 	 * <tr> | ||||
| 	 * <td>{@code time stamp}</td> | ||||
| 	 * <td>{@code Instant.now()}</td> | ||||
| 	 * <tr> | ||||
| 	 * <tr> | ||||
| 	 * <td>{@code text}</td> | ||||
| 	 * <td>{@code ""}</td> | ||||
| 	 * <tr> | ||||
| 	 * </table> | ||||
| 	 * {@code time stamp} | ||||
| 	 * {@code Instant.now()} | ||||
| 	 * <br> | ||||
| 	 * {@code text} | ||||
| 	 * {@code ""} | ||||
| 	 * | ||||
| 	 * @param group          the {@link Group} that is used to fill the map of | ||||
| 	 *                       member statuses | ||||
|   | ||||
		Reference in New Issue
	
	Block a user