Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow drag and drop of image file in prompt textArea #566

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation co
conversation,
EditorUtil.getSelectedEditorSelectedText(project),
this);
userPromptTextArea = new UserPromptTextArea(this::handleSubmit, totalTokensPanel);
userPromptTextArea = new UserPromptTextArea(project, this::handleSubmit, totalTokensPanel);
rootPanel = createRootPanel();
userPromptTextArea.requestFocusInWindow();
userPromptTextArea.requestFocus();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBTextArea;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.JBUI.CurrentTheme.DragAndDrop;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.actions.AttachImageAction;
Expand All @@ -34,9 +36,19 @@
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
Expand All @@ -45,6 +57,7 @@
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.text.BadLocationException;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.NotNull;

public class UserPromptTextArea extends JPanel {
Expand All @@ -58,12 +71,16 @@ public class UserPromptTextArea extends JPanel {
new AtomicReference<>();
private final JBTextArea textArea;
private final int textAreaRadius = 16;
private final Project project;
private final Consumer<String> onSubmit;
private IconActionButton stopButton;
private boolean submitEnabled = true;
private boolean isDragActive = false;

public UserPromptTextArea(Consumer<String> onSubmit, TotalTokensPanel totalTokensPanel) {
public UserPromptTextArea(Project project, Consumer<String> onSubmit,
TotalTokensPanel totalTokensPanel) {
super(new BorderLayout());
this.project = project;
this.onSubmit = onSubmit;

textArea = new JBTextArea();
Expand All @@ -72,7 +89,7 @@ public UserPromptTextArea(Consumer<String> onSubmit, TotalTokensPanel totalToken
textArea.setBackground(BACKGROUND_COLOR);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.getEmptyText().setText(CodeGPTBundle.get("toolwindow.chat.textArea.emptyText"));
resetEmptyText();
textArea.setBorder(JBUI.Borders.empty(8, 4));
UIUtil.addShiftEnterInputMap(textArea, new AbstractAction() {
@Override
Expand Down Expand Up @@ -140,9 +157,15 @@ protected void paintComponent(Graphics g) {
protected void paintBorder(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(JBUI.CurrentTheme.ActionButton.focusedBorder());
if (textArea.isFocusOwner()) {
g2.setStroke(new BasicStroke(1.5F));
if (isDragActive) {
g2.setColor(DragAndDrop.BORDER_COLOR);
g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
0, new float[]{9}, 0));
} else {
g2.setColor(JBUI.CurrentTheme.ActionButton.focusedBorder());
if (textArea.isFocusOwner()) {
g2.setStroke(new BasicStroke(1.5F));
}
}
g2.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, textAreaRadius, textAreaRadius);
}
Expand Down Expand Up @@ -198,11 +221,70 @@ public void actionPerformed(@NotNull AnActionEvent e) {
}));
if (isImageActionSupported()) {
iconsPanel.add(new IconActionButton(new AttachImageAction()));
if (!ApplicationManager.getApplication().isUnitTestMode()) {
setDropTarget(new DropTarget() {
@Override
public synchronized void dragEnter(DropTargetDragEvent evt) {
isDragActive = true;
var t = evt.getTransferable();
var isSupportedFile = false;
try {
List<File> files = (List<File>) t.getTransferData(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, apparently this results in null on macOS, or at least it doesn't work for me. 😞

Here's bit of a context: https://stackoverflow.com/a/64926775

I tried using the latest JB JRE while running the plugin locally, as suggested. I also installed the plugin manually on my actual IDE, but both scenarios resulted in a NPE. I haven't tried the first option yet tho.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm that is quite annoying. Unfortunately I do not have macOS so I can't try it out myself 🙃

DataFlavor.javaFileListFlavor);
isSupportedFile = files.size() == 1
&& AttachImageAction.SUPPORTED_EXTENSIONS.contains(
FilenameUtils.getExtension(files.get(0).getName().toLowerCase()));
} catch (UnsupportedFlavorException | IOException | ClassCastException ex) {
LOG.debug("Unable to get image file list:", ex);
}
if (isSupportedFile) {
textArea.getEmptyText()
.setText(CodeGPTBundle.get("toolwindow.chat.textArea.drag.allowed"));
evt.acceptDrag(DnDConstants.ACTION_COPY);
} else {
textArea.getEmptyText()
.setText(CodeGPTBundle.get("toolwindow.chat.textArea.drag.notAllowed"));
evt.rejectDrag();
}
repaint();
}

@Override
public synchronized void dragExit(DropTargetEvent dte) {
isDragActive = false;
resetEmptyText();
repaint();
}

@Override
public synchronized void drop(DropTargetDropEvent evt) {
isDragActive = false;
resetEmptyText();
try {
evt.acceptDrop(DnDConstants.ACTION_COPY);
Transferable transferable = evt.getTransferable();
List<File> files = (List<File>) transferable.getTransferData(
DataFlavor.javaFileListFlavor);
if (files.size() != 1) {
return;
}
AttachImageAction.addImageAttachment(project, files.get(0).getAbsolutePath());
} catch (UnsupportedFlavorException | IOException | ClassCastException ex) {
LOG.error("Unable to drop image file:", ex);
}
}
});
textArea.getDropTarget().setActive(false);
}
}
iconsPanel.add(stopButton);
add(iconsPanel, BorderLayout.EAST);
}

private void resetEmptyText() {
textArea.getEmptyText().setText(CodeGPTBundle.get("toolwindow.chat.textArea.emptyText"));
}

private boolean isImageActionSupported() {
var selectedService = GeneralSettings.getSelectedService();
if (selectedService == ANTHROPIC || selectedService == OLLAMA) {
Expand Down
23 changes: 16 additions & 7 deletions src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.project.Project
import ee.carlrobert.codegpt.CodeGPTBundle
import ee.carlrobert.codegpt.CodeGPTKeys
import ee.carlrobert.codegpt.Icons
Expand All @@ -19,12 +20,7 @@ class AttachImageAction : AnAction(
FileChooser.chooseFiles(createSingleImageFileDescriptor(), e.project, null).also { files ->
if (files.isNotEmpty()) {
check(files.size == 1) { "Expected exactly one file to be selected" }
e.project?.let { project ->
CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH[project] = files.first().path
project.messageBus
.syncPublisher(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC)
.imageAttached(files.first().path)
}
e.project?.let { addImageAttachment(it, files.first().path) }
}
}
}
Expand All @@ -33,8 +29,21 @@ class AttachImageAction : AnAction(
true, false, false, false, false, false
).apply {
withFileFilter { file ->
file.extension in listOf("jpg", "jpeg", "png")
file.extension in SUPPORTED_EXTENSIONS
}
withTitle(CodeGPTBundle.get("imageFileChooser.title"))
}

companion object {
@JvmField
var SUPPORTED_EXTENSIONS = listOf("jpg", "jpeg", "png")

@JvmStatic
fun addImageAttachment(project: Project, filePath: String) {
CodeGPTKeys.IMAGE_ATTACHMENT_FILE_PATH[project] = filePath
project.messageBus
.syncPublisher(AttachImageNotifier.IMAGE_ATTACHMENT_FILE_PATH_TOPIC)
.imageAttached(filePath)
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/messages/codegpt.properties
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ toolwindow.chat.youProCheckBox.enable=Turn on for complex queries
toolwindow.chat.youProCheckBox.disable=Turn off for faster responses
toolwindow.chat.youProCheckBox.notAllowed=Enable by subscribing to YouPro plan
toolwindow.chat.textArea.emptyText=Ask me anything...
toolwindow.chat.textArea.drag.allowed=Drop image file here to add it to the prompt
toolwindow.chat.textArea.drag.notAllowed=Only a single image file (.png, .jpg, .jpeg) is supported!
service.codegpt.title=CodeGPT
service.openai.title=OpenAI
service.custom.openai.title=Custom OpenAI
Expand Down