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 42e9f87..c022ac7 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -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))); @@ -555,7 +561,10 @@ public final class ChatScene implements EventListener, Restorable { // 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) checkPostConditions(e); + if (messageTextArea.getText().length() == 1 && e != null) checkPostConditions(e); + + // This is needed for the messageTA context menu + else if (e == null) checkPostConditions(false); } /** diff --git a/client/src/main/java/envoy/client/ui/custom/TextInputContextMenu.java b/client/src/main/java/envoy/client/ui/custom/TextInputContextMenu.java new file mode 100644 index 0000000..02b1889 --- /dev/null +++ b/client/src/main/java/envoy/client/ui/custom/TextInputContextMenu.java @@ -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. + *

+ * Current options are: + *

+ *

+ * Project: client
+ * File: TextInputContextMenu.java
+ * Created: 20.09.2020
+ * + * @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: + *

+ * + * @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 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 addAction(Consumer originalAction, Consumer additionalAction) { + return e -> { originalAction.accept(e); additionalAction.accept(e); }; + } +} diff --git a/common/src/main/java/envoy/data/MessageBuilder.java b/common/src/main/java/envoy/data/MessageBuilder.java index 2cddc6a..b1a3f74 100644 --- a/common/src/main/java/envoy/data/MessageBuilder.java +++ b/common/src/main/java/envoy/data/MessageBuilder.java @@ -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:
*
- * - * - * - * - * - * - * - * - * - * - * - * - * - *
{@code date}{@code Instant.now()} and {@code null} for {@code receivedDate} and - * {@code readDate}
{@code text}{@code ""}
{@code status}{@code MessageStatus.WAITING}
+ * {@code date} + * {@code Instant.now()} and {@code null} for {@code receivedDate} and + * {@code readDate} + *
+ * {@code text} + * {@code ""} + *
+ * {@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:
*
- * - * - * - * - * - * - * - * - * - *
{@code time stamp}{@code Instant.now()}
{@code text}{@code ""}
+ * {@code time stamp} + * {@code Instant.now()} + *
+ * {@code text} + * {@code ""} + *
* * @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:
*
- * - * - * - * - * - * - * - * - * - *
{@code time stamp}{@code Instant.now()}
{@code text}{@code ""}
+ * {@code time stamp} + * {@code Instant.now()} + *
+ * {@code text} + * {@code ""} * * @param group the {@link Group} that is used to fill the map of * member statuses