Skip to content

Commit 4783fa5

Browse files
feat: add right-click copy context menu to AI chat messages (#14722)
* feat: add right-click copy context menu to AI chat messages * feat: add copy context menu and ChatMessage content utility * refactor: use ClipBoardManager and specific exception handling for AI chat copy * refactor: use @Inject and early returns for AI chat copy logic * refactor: extract copy/selection logic into methods and align style * docs: add explanation for error handling * refactor: preserve AI chat selection using event filters and copySelectedText * remove dead code and commented-out selection variables * style: remove extra blank line in constructor * add changelog entry for AI chat copy selection * add changelog entry for AI chat copy selection * replace static import with MouseEvent class import --------- Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com>
1 parent f4d14d6 commit 4783fa5

File tree

3 files changed

+79
-0
lines changed

3 files changed

+79
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1414
- We fixed an issue where a redundant validation listener was causing duplicate error dialogs when invalid BibTeX source was detected in the SourceTab. [#14805](https://github.com/JabRef/jabref/issues/14805)
1515
- We added support for selecting citation fetcher in Citations Tab. [#14430](https://github.com/JabRef/jabref/issues/14430)
1616
- In the "New Entry" dialog the identifier type is now automatically updated on typing. [#14660](https://github.com/JabRef/jabref/issues/14660)
17+
- We added the ability to copy selected text from AI chat interface. [#14655](https://github.com/JabRef/jabref/issues/14655)
1718
- We added cover images for books, which will display in entry previews if available, and can be automatically downloaded when adding an entry via ISBN. [#10120](https://github.com/JabRef/jabref/issues/10120)
1819
- REST-API: Added more commands (`selectentries`, `open`, `focus`).
1920
- REST-API: Added the possibility to trigger the import dialog

jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,26 @@
77
import javafx.fxml.FXML;
88
import javafx.geometry.NodeOrientation;
99
import javafx.geometry.Pos;
10+
import javafx.scene.control.ContextMenu;
1011
import javafx.scene.control.Label;
12+
import javafx.scene.control.MenuItem;
13+
import javafx.scene.input.MouseEvent;
1114
import javafx.scene.layout.HBox;
1215
import javafx.scene.layout.Pane;
1316
import javafx.scene.layout.Priority;
1417
import javafx.scene.layout.VBox;
1518

19+
import org.jabref.gui.clipboard.ClipBoardManager;
1620
import org.jabref.gui.util.MarkdownTextFlow;
21+
import org.jabref.logic.ai.util.ChatMessageUtils;
1722
import org.jabref.logic.ai.util.ErrorMessage;
1823
import org.jabref.logic.l10n.Localization;
1924

2025
import com.airhacks.afterburner.views.ViewLoader;
2126
import dev.langchain4j.data.message.AiMessage;
2227
import dev.langchain4j.data.message.ChatMessage;
2328
import dev.langchain4j.data.message.UserMessage;
29+
import jakarta.inject.Inject;
2430
import org.slf4j.Logger;
2531
import org.slf4j.LoggerFactory;
2632

@@ -37,6 +43,7 @@ public class ChatMessageComponent extends HBox {
3743
@FXML private VBox buttonsVBox;
3844

3945
private final MarkdownTextFlow markdownTextFlow;
46+
@Inject private ClipBoardManager clipBoardManager;
4047

4148
public ChatMessageComponent() {
4249
ViewLoader.view(this)
@@ -53,6 +60,7 @@ public ChatMessageComponent() {
5360
markdownContentPane.getChildren().add(markdownTextFlow);
5461
markdownContentPane.minHeightProperty().bind(markdownTextFlow.heightProperty());
5562
markdownContentPane.prefHeightProperty().bind(markdownTextFlow.heightProperty());
63+
setupContextMenu();
5664
}
5765

5866
public ChatMessageComponent(ChatMessage chatMessage, Consumer<ChatMessageComponent> onDeleteCallback) {
@@ -61,6 +69,36 @@ public ChatMessageComponent(ChatMessage chatMessage, Consumer<ChatMessageCompone
6169
setOnDelete(onDeleteCallback);
6270
}
6371

72+
private void setupContextMenu() {
73+
ContextMenu contextMenu = new ContextMenu();
74+
MenuItem copyItem = new MenuItem(Localization.lang("Copy"));
75+
contextMenu.getItems().add(copyItem);
76+
77+
// 1. Capture and LOCK the selection state
78+
markdownContentPane.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
79+
if (event.isSecondaryButtonDown() && markdownTextFlow.isSelectionActive()) {
80+
// Consume the event to prevent JavaFX from clearing the selection highlight
81+
event.consume();
82+
// Manually trigger the context menu since we consumed the event that usually triggers it
83+
contextMenu.show(markdownContentPane, event.getScreenX(), event.getScreenY());
84+
}
85+
});
86+
87+
copyItem.setOnAction(_ -> {
88+
if (markdownTextFlow.isSelectionActive()) {
89+
markdownTextFlow.copySelectedText();
90+
} else {
91+
copyFullMessage();
92+
}
93+
});
94+
95+
markdownContentPane.setOnContextMenuRequested(event -> {
96+
if (!markdownTextFlow.isSelectionActive()) {
97+
contextMenu.show(markdownContentPane, event.getScreenX(), event.getScreenY());
98+
}
99+
});
100+
}
101+
64102
public void setChatMessage(ChatMessage chatMessage) {
65103
this.chatMessage.set(chatMessage);
66104
}
@@ -119,4 +157,12 @@ private void onDeleteClick() {
119157
private void setColor(String fillColor, String borderColor) {
120158
vBox.setStyle("-fx-background-color: " + fillColor + "; -fx-border-radius: 10; -fx-background-radius: 10; -fx-border-color: " + borderColor + "; -fx-border-width: 3;");
121159
}
160+
161+
private void copyFullMessage() {
162+
ChatMessageUtils.getContent(chatMessage.get()).ifPresent(content -> {
163+
if (!content.isEmpty()) {
164+
clipBoardManager.setContent(content);
165+
}
166+
});
167+
}
122168
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.jabref.logic.ai.util;
2+
3+
import java.util.Optional;
4+
5+
import dev.langchain4j.data.message.AiMessage;
6+
import dev.langchain4j.data.message.ChatMessage;
7+
import dev.langchain4j.data.message.UserMessage;
8+
9+
public class ChatMessageUtils {
10+
11+
private ChatMessageUtils() {
12+
}
13+
14+
public static Optional<String> getContent(ChatMessage chatMessage) {
15+
if (chatMessage == null) {
16+
return Optional.empty();
17+
}
18+
19+
String content = switch (chatMessage) {
20+
case UserMessage user ->
21+
user.singleText();
22+
case AiMessage ai ->
23+
ai.text();
24+
case ErrorMessage err ->
25+
err.getText();
26+
default ->
27+
null;
28+
};
29+
30+
return Optional.ofNullable(content);
31+
}
32+
}

0 commit comments

Comments
 (0)