diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index a5088d948..557aa2cd7 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -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(); diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java index 7570f8872..09c633350 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/UserPromptTextArea.java @@ -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; @@ -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; @@ -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 { @@ -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 onSubmit; private IconActionButton stopButton; private boolean submitEnabled = true; + private boolean isDragActive = false; - public UserPromptTextArea(Consumer onSubmit, TotalTokensPanel totalTokensPanel) { + public UserPromptTextArea(Project project, Consumer onSubmit, + TotalTokensPanel totalTokensPanel) { super(new BorderLayout()); + this.project = project; this.onSubmit = onSubmit; textArea = new JBTextArea(); @@ -72,7 +89,7 @@ public UserPromptTextArea(Consumer 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 @@ -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); } @@ -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 files = (List) t.getTransferData( + 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 files = (List) 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) { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt index 509ed8c23..9f8fc3023 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/AttachImageAction.kt @@ -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 @@ -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) } } } } @@ -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) + } + } } \ No newline at end of file diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 02a92f314..8c317453d 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -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