diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3fbce39..2f773e98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Fixed drag-and-drop into text fields: files are now inserted at the text caret position instead of where the mouse was dropped
- Fixed Accept/Decline and Yes/No button colors to respect the IDE Look and Feel theme (dark themes no longer show clashing hardcoded colors)
- Added session management when opening a session from the toolbar and when closing a tab via the Close button
- Added configurable session mode: choose to resume the last session or start a new one
diff --git a/pom.xml b/pom.xml
index c6e262f8..aa9791eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.21.14-SNAPSHOT
+ 0.21.18-SNAPSHOT
nbm
Netbeans Claude Code GUI
NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/FileDropHandler.java b/src/main/java/io/github/nbclaudecodegui/ui/common/FileDropHandler.java
index 983c1d6b..07fcdf76 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/common/FileDropHandler.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/common/FileDropHandler.java
@@ -198,7 +198,20 @@ public boolean importData(TransferSupport support) {
LOG.fine("importData: component=" + support.getComponent().getClass().getSimpleName());
Transferable t = support.getTransferable();
JTextComponent textComponent = findTextComponent(support.getComponent());
- return doImport(t, textComponent);
+ int insertPos = textComponent != null ? textComponent.getCaretPosition() : 0;
+ return doImport(t, textComponent, insertPos);
+ }
+
+ /**
+ * Called by our custom {@link java.awt.dnd.DropTargetAdapter} in place of the standard
+ * Swing drop pipeline. Because our adapter never calls {@code setDropLocation}, the caret
+ * stays at the position the user left it — so we can read it here and insert there.
+ */
+ void handleDrop(java.awt.dnd.DropTargetDropEvent dtde, JTextComponent tc) {
+ dtde.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE);
+ int insertPos = tc.getCaretPosition();
+ boolean ok = doImport(dtde.getTransferable(), tc, insertPos);
+ dtde.dropComplete(ok);
}
// -------------------------------------------------------------------------
@@ -223,19 +236,19 @@ public boolean importFromClipboard(JTextComponent textComponent) {
boolean hasText = t.isDataFlavorSupported(DataFlavor.stringFlavor);
LOG.fine("importFromClipboard: hasFiles=" + hasFiles + " hasImage=" + hasImage + " hasText=" + hasText);
if (!hasFiles && !hasImage && !hasText) return false;
- return doImport(t, textComponent);
+ return doImport(t, textComponent, textComponent.getCaretPosition());
}
// -------------------------------------------------------------------------
// Shared import logic
// -------------------------------------------------------------------------
- private boolean doImport(Transferable t, JTextComponent textComponent) {
+ private boolean doImport(Transferable t, JTextComponent textComponent, int insertPos) {
try {
Node[] nodes = extractNodes(t);
if (nodes != null && nodes.length > 0) {
LOG.fine("doImport: nodes branch, count=" + nodes.length);
- return importNodes(nodes, textComponent);
+ return importNodes(nodes, textComponent, insertPos);
}
if (t.isDataFlavorSupported(DataFlavor.imageFlavor)) {
Image img = (Image) t.getTransferData(DataFlavor.imageFlavor);
@@ -243,7 +256,7 @@ private boolean doImport(Transferable t, JTextComponent textComponent) {
LOG.fine("doImport: image branch");
File tmp = writeTempPng(img);
if (textComponent != null) {
- insertAtCaret(textComponent, "@" + tmp.getAbsolutePath());
+ insertAtCaret(textComponent, "@" + tmp.getAbsolutePath(), insertPos);
}
return true;
}
@@ -265,14 +278,14 @@ private boolean doImport(Transferable t, JTextComponent textComponent) {
}
if (paths.length() > 0 && textComponent != null) {
- insertAtCaret(textComponent, paths.toString());
+ insertAtCaret(textComponent, paths.toString(), insertPos);
}
return true;
}
if (t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
LOG.fine("doImport: string branch");
String s = (String) t.getTransferData(DataFlavor.stringFlavor);
- if (s != null && textComponent != null) { insertAtCaret(textComponent, s); return true; }
+ if (s != null && textComponent != null) { insertAtCaret(textComponent, s, insertPos); return true; }
}
} catch (Exception ex) {
// Import failed; fall through to return false
@@ -297,6 +310,11 @@ private boolean doImport(Transferable t, JTextComponent textComponent) {
*
*/
boolean importNodes(Node[] nodes, JTextComponent textComponent) {
+ int pos = textComponent != null ? textComponent.getCaretPosition() : 0;
+ return importNodes(nodes, textComponent, pos);
+ }
+
+ private boolean importNodes(Node[] nodes, JTextComponent textComponent, int insertPos) {
LOG.fine("importNodes: entry, count=" + (nodes == null ? 0 : nodes.length));
if (nodes == null || nodes.length == 0) return false;
String wdStr = workingDirSupplier.get();
@@ -393,7 +411,7 @@ boolean importNodes(Node[] nodes, JTextComponent textComponent) {
}
if (inlineText.length() > 0 && textComponent != null) {
- insertAtCaret(textComponent, inlineText.toString());
+ insertAtCaret(textComponent, inlineText.toString(), insertPos);
}
LOG.fine("importNodes: returning " + (inlineText.length() > 0) + ", text='" + inlineText + "'");
return inlineText.length() > 0;
@@ -438,9 +456,8 @@ private File writeTempPng(Image img) throws IOException {
return tmp;
}
- private static void insertAtCaret(JTextComponent textComponent, String text) {
+ private static void insertAtCaret(JTextComponent textComponent, String text, int pos) {
javax.swing.SwingUtilities.invokeLater(() -> {
- int pos = textComponent.getCaretPosition();
try {
String before = textComponent.getText(0, pos);
String toInsert = (!before.isEmpty() && !before.endsWith(" ") && !before.endsWith("\n"))
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/TextComponentDecorator.java b/src/main/java/io/github/nbclaudecodegui/ui/common/TextComponentDecorator.java
index ba196623..d0c303b6 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/common/TextComponentDecorator.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/common/TextComponentDecorator.java
@@ -54,6 +54,31 @@ public void setup() {
dropHandler = new FileDropHandler(wdSupplier, tc.getTransferHandler());
tc.setTransferHandler(dropHandler);
+ // Replace Swing's SwingDropTarget with our own that never calls setDropLocation,
+ // so the caret stays frozen at its original position during the entire drag.
+ tc.setDropTarget(new java.awt.dnd.DropTarget(tc,
+ java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE,
+ new java.awt.dnd.DropTargetAdapter() {
+ @Override
+ public void dragEnter(java.awt.dnd.DropTargetDragEvent e) {
+ if (dropHandler.canImportTransferable(e.getTransferable()))
+ e.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE);
+ else
+ e.rejectDrag();
+ }
+ @Override
+ public void dragOver(java.awt.dnd.DropTargetDragEvent e) {
+ if (dropHandler.canImportTransferable(e.getTransferable()))
+ e.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE);
+ else
+ e.rejectDrag();
+ }
+ @Override
+ public void drop(java.awt.dnd.DropTargetDropEvent e) {
+ dropHandler.handleDrop(e, tc);
+ }
+ }, true));
+
// 2. @-completion
AtCompletionPopup.install(tc, wdSupplier);
diff --git a/src/test/java/io/github/nbclaudecodegui/ui/common/FileDropHandlerTest.java b/src/test/java/io/github/nbclaudecodegui/ui/common/FileDropHandlerTest.java
index 28c65f55..b70467ab 100644
--- a/src/test/java/io/github/nbclaudecodegui/ui/common/FileDropHandlerTest.java
+++ b/src/test/java/io/github/nbclaudecodegui/ui/common/FileDropHandlerTest.java
@@ -782,6 +782,61 @@ void importNodesFilesWindowNodeInsideWorkDirInsertsRelativePath() throws Excepti
});
}
+ // -------------------------------------------------------------------------
+ // Tests: insert at explicit position (DnD caret-save fix)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Verifies that doImport(Transferable, JTextComponent, int) inserts at the
+ * given position, NOT at the text component's current caret position.
+ * This is the core of the DnD caret-save fix: the drag-hover caret movement
+ * must be ignored; insertion must happen where the caret was before the drag.
+ */
+ @Test
+ void dropFileInsertsAtExplicitPositionIgnoringCurrentCaret() throws Exception {
+ File inside = new File(workDir, "file.txt");
+ inside.createNewFile();
+
+ SwingUtilities.invokeAndWait(() -> {
+ area.setText("hello world");
+ // Simulate Swing moving the caret to the drop location (end of text)
+ area.setCaretPosition(11);
+ // Call doImport with insertPos=5 (the saved pre-drag caret position)
+ java.awt.datatransfer.Transferable t = fileListTransferable(List.of(inside));
+ callDoImportAt(handler, t, area, 5);
+ });
+
+ SwingUtilities.invokeAndWait(() -> {
+ String text = area.getText();
+ assertTrue(text.contains("file.txt"), "path must appear in text, got: '" + text + "'");
+ // The path must be inserted inside "hello world", not appended at the end
+ assertTrue(text.startsWith("hello"),
+ "text must start with 'hello' (insertion at pos 5), got: '" + text + "'");
+ int pathIdx = text.indexOf("file.txt");
+ assertTrue(pathIdx > 0 && pathIdx < text.indexOf("world"),
+ "path must be inserted between 'hello' and 'world', got: '" + text + "'");
+ });
+ }
+
+ /**
+ * Calls the package-private {@code doImport(Transferable, JTextComponent, int)} via reflection.
+ */
+ private static boolean callDoImportAt(FileDropHandler handler,
+ java.awt.datatransfer.Transferable t,
+ JTextArea area, int insertPos) {
+ try {
+ java.lang.reflect.Method m = FileDropHandler.class.getDeclaredMethod(
+ "doImport",
+ java.awt.datatransfer.Transferable.class,
+ javax.swing.text.JTextComponent.class,
+ int.class);
+ m.setAccessible(true);
+ return (boolean) m.invoke(handler, t, area, insertPos);
+ } catch (Exception ex) {
+ throw new RuntimeException("Could not invoke FileDropHandler.doImport(t, tc, pos)", ex);
+ }
+ }
+
// -------------------------------------------------------------------------
// Tests: Cut/Copy — export delegation to original TransferHandler
// -------------------------------------------------------------------------
@@ -845,7 +900,8 @@ void canImportBlankAbstractNodeReturnsFalse() throws Exception {
}
/**
- * Calls the package-private {@code doImport(Transferable, JTextComponent)} via reflection.
+ * Calls the package-private {@code doImport(Transferable, JTextComponent, int)} via reflection,
+ * using the current caret position as the insert position.
*/
private static boolean callDoImport(FileDropHandler handler,
java.awt.datatransfer.Transferable t,
@@ -854,9 +910,10 @@ private static boolean callDoImport(FileDropHandler handler,
java.lang.reflect.Method m = FileDropHandler.class.getDeclaredMethod(
"doImport",
java.awt.datatransfer.Transferable.class,
- javax.swing.text.JTextComponent.class);
+ javax.swing.text.JTextComponent.class,
+ int.class);
m.setAccessible(true);
- return (boolean) m.invoke(handler, t, area);
+ return (boolean) m.invoke(handler, t, area, area.getCaretPosition());
} catch (Exception ex) {
throw new RuntimeException("Could not invoke FileDropHandler.doImport", ex);
}