Skip to content

Commit

Permalink
Fix clipboard pasting
Browse files Browse the repository at this point in the history
Because of the way JavaFX inspects the clipboard, sometimes CTRL-V would fail if the clipboard contains some complex data (but still a string or an image). Then it would work the second time it's pressed. We bypass JavaFX and process it directly from now on.

Also images can be pasted from the context menu.
  • Loading branch information
zapek committed Jan 7, 2025
1 parent e12bd96 commit 66ed45d
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())));
Expand All @@ -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()));

Expand Down Expand Up @@ -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();
}
}
Expand All @@ -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) + "\"/>");
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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.*;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
}
Expand All @@ -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);
Expand Down
38 changes: 28 additions & 10 deletions ui/src/main/java/io/xeres/ui/custom/EditorView.java
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(), "![](" + imgData + ")");

event.consume();
}
}
Expand All @@ -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(), "![](" + imgData + ")");

yield true;
}
case String string ->
{
textInputControl.insertText(textInputControl.getCaretPosition(), string);
yield true;
}
default -> false;
};
}

/**
* Inserts a new line without cutting the current line.
*/
Expand Down
67 changes: 62 additions & 5 deletions ui/src/main/java/io/xeres/ui/support/clipboard/ClipboardUtils.java
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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
{
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 66ed45d

Please sign in to comment.