diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java index bad44f9b..fcebd90a 100644 --- a/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 by David Gerber - https://zapek.com + * Copyright (c) 2019-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -54,6 +54,7 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.control.*; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; @@ -204,10 +205,15 @@ public ChatViewController(MessageClient messageClient, ChatClient chatClient, Pr } @Override - public void initialize() throws IOException + public void initialize() { - profileClient.getOwn().doOnSuccess(profile -> nickname = profile.getName()) // XXX: we shouldn't go further until nickname is set. maybe it should be a parameter + profileClient.getOwn().doOnSuccess(profile -> Platform.runLater(() -> initializeReally(profile.getName()))) .subscribe(); + } + + private void initializeReally(String nickname) + { + this.nickname = nickname; var root = new TreeItem<>(new RoomHolder()); //noinspection unchecked @@ -253,7 +259,14 @@ public void initialize() throws IOException }); var loader = new FXMLLoader(getClass().getResource("/view/chat/chat_roominfo.fxml"), bundle); - roomInfoView = loader.load(); + try + { + roomInfoView = loader.load(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } chatRoomInfoController = loader.getController(); lastTypingTimeline = new Timeline(new KeyFrame(javafx.util.Duration.seconds(TYPING_NOTIFICATION_DELAY.getSeconds()))); @@ -268,7 +281,7 @@ public void initialize() throws IOException previewCancel.setOnAction(event -> cancelImage()); send.addEventHandler(KeyEvent.KEY_PRESSED, this::handleInputKeys); - TextInputControlUtils.addEnhancedInputContextMenu(send, locationClient); + TextInputControlUtils.addEnhancedInputContextMenu(send, locationClient, this::handlePaste); invite.setOnAction(event -> windowManager.openInvite(selectedRoom.getId())); @@ -688,14 +701,8 @@ private void handleInputKeys(KeyEvent event) if (PASTE_KEY.match(event)) { - var image = ClipboardUtils.getImageFromClipboard(); - if (image != null) + if (handlePaste(send)) { - imagePreview.setImage(image); - - ImageUtils.limitMaximumImageSize(imagePreview, PREVIEW_IMAGE_WIDTH_MAX * PREVIEW_IMAGE_HEIGHT_MAX); - - setPreviewGroupVisibility(true); event.consume(); } } @@ -711,6 +718,29 @@ else if (BACKSPACE_KEY.match(event) && imagePreview.getImage() != null) } } + private boolean handlePaste(TextInputControl textInputControl) + { + var object = ClipboardUtils.getSupportedObjectFromClipboard(); + return switch (object) + { + case Image image -> + { + imagePreview.setImage(image); + + ImageUtils.limitMaximumImageSize(imagePreview, PREVIEW_IMAGE_WIDTH_MAX * PREVIEW_IMAGE_HEIGHT_MAX); + + setPreviewGroupVisibility(true); + yield true; + } + case String string -> + { + textInputControl.insertText(textInputControl.getCaretPosition(), string); + yield true; + } + default -> false; + }; + } + private void sendImage() { sendChatMessage("<img src=\"" + ImageUtils.writeImageAsJpegData(imagePreview.getImage(), MESSAGE_MAXIMUM_SIZE) + "\"/>"); diff --git a/ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java b/ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java index c1bded04..1de58b56 100644 --- a/ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java +++ b/ui/src/main/java/io/xeres/ui/controller/contact/ContactViewController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 by David Gerber - https://zapek.com + * Copyright (c) 2024-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -358,7 +358,7 @@ private void setupContactSearch() searchClear.setCursor(Cursor.HAND); searchClear.setOnMouseClicked(event -> searchTextField.clear()); - TextInputControlUtils.addEnhancedInputContextMenu(searchTextField, null); + TextInputControlUtils.addEnhancedInputContextMenu(searchTextField, null, null); searchTextField.textProperty().addListener((observable, oldValue, newValue) -> contactFilter.setNameFilter(newValue)); searchTextField.lengthProperty().addListener((observable, oldValue, newValue) -> { if (newValue.intValue() > 0) diff --git a/ui/src/main/java/io/xeres/ui/controller/file/FileSearchViewController.java b/ui/src/main/java/io/xeres/ui/controller/file/FileSearchViewController.java index ffca0cac..650723b8 100644 --- a/ui/src/main/java/io/xeres/ui/controller/file/FileSearchViewController.java +++ b/ui/src/main/java/io/xeres/ui/controller/file/FileSearchViewController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 by David Gerber - https://zapek.com + * Copyright (c) 2024-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -80,7 +80,7 @@ public FileSearchViewController(FileClient fileClient, NotificationClient notifi @Override public void initialize() { - TextInputControlUtils.addEnhancedInputContextMenu(search, null); + TextInputControlUtils.addEnhancedInputContextMenu(search, null, null); search.setOnKeyPressed(event -> { if (event.getCode() == KeyCode.ENTER) { diff --git a/ui/src/main/java/io/xeres/ui/controller/id/AddRsIdWindowController.java b/ui/src/main/java/io/xeres/ui/controller/id/AddRsIdWindowController.java index f5567f43..4015bda8 100644 --- a/ui/src/main/java/io/xeres/ui/controller/id/AddRsIdWindowController.java +++ b/ui/src/main/java/io/xeres/ui/controller/id/AddRsIdWindowController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 by David Gerber - https://zapek.com + * Copyright (c) 2019-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -128,7 +128,7 @@ public void initialize() debouncer.setOnFinished(event -> checkRsId(newValue)); debouncer.playFromStart(); }); - TextInputControlUtils.addEnhancedInputContextMenu(rsIdTextArea, null); + TextInputControlUtils.addEnhancedInputContextMenu(rsIdTextArea, null, null); profileClient.getOwn() .doOnSuccess(profile -> ownProfile = profile) diff --git a/ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java b/ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java index 4551e5e5..cd108c22 100644 --- a/ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java +++ b/ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 by David Gerber - https://zapek.com + * Copyright (c) 2019-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -55,6 +55,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.TextArea; +import javafx.scene.control.TextInputControl; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.*; @@ -161,7 +162,7 @@ public void initialize() .subscribe(); send.addEventHandler(KeyEvent.KEY_PRESSED, this::handleInputKeys); - TextInputControlUtils.addEnhancedInputContextMenu(send, null); + TextInputControlUtils.addEnhancedInputContextMenu(send, null, this::handlePaste); addImage.setOnAction(event -> { var fileChooser = new FileChooser(); @@ -389,10 +390,8 @@ private void handleInputKeys(KeyEvent event) { if (PASTE_KEY.match(event)) { - var image = ClipboardUtils.getImageFromClipboard(); - if (image != null) + if (handlePaste(send)) { - sendImageViewToMessage(new ImageView(image)); event.consume(); } } @@ -417,6 +416,25 @@ else if (event.getCode() == KeyCode.ENTER) } } + private boolean handlePaste(TextInputControl textInputControl) + { + var object = ClipboardUtils.getSupportedObjectFromClipboard(); + return switch (object) + { + case Image image -> + { + sendImageViewToMessage(new ImageView(image)); + yield true; + } + case String string -> + { + textInputControl.insertText(textInputControl.getCaretPosition(), string); + yield true; + } + default -> false; + }; + } + private void sendImageViewToMessage(ImageView imageView) { ImageUtils.limitMaximumImageSize(imageView, IMAGE_WIDTH_MAX * IMAGE_HEIGHT_MAX); diff --git a/ui/src/main/java/io/xeres/ui/custom/EditorView.java b/ui/src/main/java/io/xeres/ui/custom/EditorView.java index 0900f808..7596025f 100644 --- a/ui/src/main/java/io/xeres/ui/custom/EditorView.java +++ b/ui/src/main/java/io/xeres/ui/custom/EditorView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 by David Gerber - https://zapek.com + * Copyright (c) 2023-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -31,6 +31,7 @@ import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.*; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; @@ -208,7 +209,7 @@ public void setReply(String reply) public void setInputContextMenu(LocationClient locationClient) { - TextInputControlUtils.addEnhancedInputContextMenu(editor, locationClient); + TextInputControlUtils.addEnhancedInputContextMenu(editor, locationClient, this::handlePaste); } public boolean isModified() @@ -396,15 +397,8 @@ private void handleInputKeys(KeyEvent event) if (PASTE_KEY.match(event)) { - var image = ClipboardUtils.getImageFromClipboard(); - if (image != null) + if (handlePaste(editor)) { - var imageView = new ImageView(image); - ImageUtils.limitMaximumImageSize(imageView, IMAGE_WIDTH_MAX * IMAGE_HEIGHT_MAX); - - var imgData = ImageUtils.writeImageAsJpegData(imageView.getImage(), IMAGE_MAXIMUM_SIZE); - editor.insertText(editor.getCaretPosition(), ""); - event.consume(); } } @@ -414,6 +408,30 @@ else if (ENTER_INSERT_KEY.match(event)) } } + private boolean handlePaste(TextInputControl textInputControl) + { + var object = ClipboardUtils.getSupportedObjectFromClipboard(); + return switch (object) + { + case Image image -> + { + var imageView = new ImageView(image); + ImageUtils.limitMaximumImageSize(imageView, IMAGE_WIDTH_MAX * IMAGE_HEIGHT_MAX); + + var imgData = ImageUtils.writeImageAsJpegData(imageView.getImage(), IMAGE_MAXIMUM_SIZE); + textInputControl.insertText(textInputControl.getCaretPosition(), ""); + + yield true; + } + case String string -> + { + textInputControl.insertText(textInputControl.getCaretPosition(), string); + yield true; + } + default -> false; + }; + } + /** * Inserts a new line without cutting the current line. */ diff --git a/ui/src/main/java/io/xeres/ui/support/clipboard/ClipboardUtils.java b/ui/src/main/java/io/xeres/ui/support/clipboard/ClipboardUtils.java index 98e77a35..60043e64 100644 --- a/ui/src/main/java/io/xeres/ui/support/clipboard/ClipboardUtils.java +++ b/ui/src/main/java/io/xeres/ui/support/clipboard/ClipboardUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 by David Gerber - https://zapek.com + * Copyright (c) 2024-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -31,11 +31,14 @@ /** * Utility class to use the clipboard. This implementation uses AWT because the clipboard support of JavaFX is, quite frankly, a - * royal piece of shit. + * royal piece of shit: + * <ul> + * <li>it fails to work with some bitmaps (for example from Telegram, Windows 10 and print screen, Chrome, ...). + * <li>it fails with data URIs because it tries to find out if the image is a supported format and even though it is, the URL is "wrong" for it. + * </ul> * <p> - * Fails to work with some bitmaps (for example from Telegram, Windows 10 and print screen, Chrome, ...). - * <p> - * Fails with data URIs because it tries to find out if the image is a supported format and even though it is, the URL is "wrong" for it. + * This one just works. Note that there still might be some warnings printed out because of the DataFlavor system that isn't compatible + * with everything. It's harmless though. */ public final class ClipboardUtils { @@ -44,6 +47,26 @@ private ClipboardUtils() throw new UnsupportedOperationException("Utility class"); } + /** + * Gets whatever is in the clipboard and supported, currently: string and JavaFX images. + * + * @return a string or an image + */ + public static Object getSupportedObjectFromClipboard() + { + Object object = getImageFromClipboard(); + if (object == null) + { + object = getStringFromClipboard(); + } + return object; + } + + /** + * Gets an image from the clipboard + * + * @return the image, or null if the clipboard is empty, or it doesn't contain an image + */ public static Image getImageFromClipboard() { var transferable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); @@ -63,11 +86,45 @@ public static Image getImageFromClipboard() return null; } + /** + * Copies an image to the clipboard. + * + * @param image the image to copy to the clipboard + */ public static void copyImageToClipboard(Image image) { Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new ImageSelection(SwingFXUtils.fromFXImage(image, null)), null); } + /** + * Gets a string from the clipboard. + * + * @return a string, or null if the clipboard is empty, or it doesn't contain a string + */ + public static String getStringFromClipboard() + { + var transferable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); + if (transferable != null && transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) + { + String string; + try + { + string = (String) transferable.getTransferData(DataFlavor.stringFlavor); + } + catch (UnsupportedFlavorException | IOException e) + { + throw new RuntimeException(e); + } + return string; + } + return null; + } + + /** + * Copies a string to the clipboard. + * + * @param text the string to copy to the clipboard + */ public static void copyTextToClipboard(String text) { Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text), null); diff --git a/ui/src/main/java/io/xeres/ui/support/util/TextInputControlUtils.java b/ui/src/main/java/io/xeres/ui/support/util/TextInputControlUtils.java index 0c71b061..da313831 100644 --- a/ui/src/main/java/io/xeres/ui/support/util/TextInputControlUtils.java +++ b/ui/src/main/java/io/xeres/ui/support/util/TextInputControlUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 by David Gerber - https://zapek.com + * Copyright (c) 2023-2025 by David Gerber - https://zapek.com * * This file is part of Xeres. * @@ -35,6 +35,7 @@ import java.util.List; import java.util.ResourceBundle; +import java.util.function.Consumer; import static io.xeres.common.dto.location.LocationConstants.OWN_LOCATION_ID; @@ -54,11 +55,11 @@ private TextInputControlUtils() * @param textInputControl the text input control * @param locationClient the location client, if null, then there will be no "Paste own ID" menu item */ - public static void addEnhancedInputContextMenu(TextInputControl textInputControl, LocationClient locationClient) + public static void addEnhancedInputContextMenu(TextInputControl textInputControl, LocationClient locationClient, Consumer<TextInputControl> pasteAction) { var contextMenu = new ContextMenu(); - contextMenu.getItems().addAll(createDefaultChatInputMenuItems(textInputControl)); + contextMenu.getItems().addAll(createDefaultChatInputMenuItems(textInputControl, pasteAction)); if (locationClient != null) { var pasteId = new MenuItem(bundle.getString("paste-id")); @@ -81,7 +82,7 @@ private static String buildRetroshareUrl(RSIdResponse rsIdResponse) return CertificateUriFactory.generate(cleanCert, rsIdResponse.name(), rsIdResponse.location()); } - private static List<MenuItem> createDefaultChatInputMenuItems(TextInputControl textInputControl) + private static List<MenuItem> createDefaultChatInputMenuItems(TextInputControl textInputControl, Consumer<TextInputControl> pasteAction) { var undo = new MenuItem(bundle.getString("undo")); undo.setGraphic(new FontIcon(MaterialDesignU.UNDO_VARIANT)); @@ -101,7 +102,16 @@ private static List<MenuItem> createDefaultChatInputMenuItems(TextInputControl t var paste = new MenuItem(bundle.getString("paste")); paste.setGraphic(new FontIcon(MaterialDesignC.CONTENT_PASTE)); - paste.setOnAction(event -> textInputControl.paste()); + paste.setOnAction(event -> { + if (pasteAction != null) + { + pasteAction.accept(textInputControl); + } + else + { + textInputControl.paste(); + } + }); var delete = new MenuItem(bundle.getString("delete")); delete.setGraphic(new FontIcon(MaterialDesignT.TRASH_CAN));