33
44package software.aws.toolkits.jetbrains.services.amazonq.toolwindow
55
6+ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
67import com.intellij.idea.AppMode
78import com.intellij.openapi.Disposable
89import com.intellij.openapi.components.service
@@ -20,6 +21,8 @@ import kotlinx.coroutines.CoroutineScope
2021import kotlinx.coroutines.flow.first
2122import kotlinx.coroutines.launch
2223import kotlinx.coroutines.withContext
24+ import software.aws.toolkits.core.utils.error
25+ import software.aws.toolkits.core.utils.getLogger
2326import software.aws.toolkits.jetbrains.core.coroutines.EDT
2427import software.aws.toolkits.jetbrains.isDeveloperMode
2528import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
@@ -44,7 +47,12 @@ import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
4447import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable
4548import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable
4649import software.aws.toolkits.resources.message
50+ import java.awt.datatransfer.DataFlavor
51+ import java.awt.dnd.DropTarget
52+ import java.awt.dnd.DropTargetDropEvent
53+ import java.io.File
4754import java.util.concurrent.CompletableFuture
55+ import javax.imageio.ImageIO.read
4856import javax.swing.JButton
4957
5058class AmazonQPanel (val project : Project , private val scope : CoroutineScope ) : Disposable {
@@ -122,12 +130,76 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di
122130
123131 withContext(EDT ) {
124132 browser.complete(
125- Browser (this @AmazonQPanel, webUri, project).also {
126- wrapper.setContent(it.component())
133+ Browser (this @AmazonQPanel, webUri, project).also { browserInstance ->
134+ wrapper.setContent(browserInstance.component())
135+
136+ // Add DropTarget to the browser component
137+ // JCEF does not propagate OS-level dragenter, dragOver and drop into DOM.
138+ // As an alternative, enabling the native drag in JCEF,
139+ // and let the native handling the drop event, and update the UI through JS bridge.
140+ val dropTarget = object : DropTarget () {
141+ override fun drop (dtde : DropTargetDropEvent ) {
142+ try {
143+ dtde.acceptDrop(dtde.dropAction)
144+ val transferable = dtde.transferable
145+ if (transferable.isDataFlavorSupported(DataFlavor .javaFileListFlavor)) {
146+ val fileList = transferable.getTransferData(DataFlavor .javaFileListFlavor) as List <* >
147+
148+ val errorMessages = mutableListOf<String >()
149+ val validImages = mutableListOf<File >()
150+ val allowedTypes = setOf (" jpg" , " jpeg" , " png" , " gif" , " webp" )
151+ val maxFileSize = 3.75 * 1024 * 1024 // 3.75MB in bytes
152+ val maxDimension = 8000
153+
154+ for (file in fileList as List <File >) {
155+ val validationResult = validateImageFile(file, allowedTypes, maxFileSize, maxDimension)
156+ if (validationResult != null ) {
157+ errorMessages.add(validationResult)
158+ } else {
159+ validImages.add(file)
160+ }
161+ }
162+
163+ // File count restriction
164+ if (validImages.size > 20 ) {
165+ errorMessages.add(" A maximum of 20 images can be added to a single message." )
166+ validImages.subList(20 , validImages.size).clear()
167+ }
168+
169+ val json = OBJECT_MAPPER .writeValueAsString(validImages)
170+ browserInstance.jcefBrowser.cefBrowser.executeJavaScript(
171+ " window.handleNativeDrop('$json ')" ,
172+ browserInstance.jcefBrowser.cefBrowser.url,
173+ 0
174+ )
175+
176+ val errorJson = OBJECT_MAPPER .writeValueAsString(errorMessages)
177+ browserInstance.jcefBrowser.cefBrowser.executeJavaScript(
178+ " window.handleNativeNotify('$errorJson ')" ,
179+ browserInstance.jcefBrowser.cefBrowser.url,
180+ 0
181+ )
182+ dtde.dropComplete(true )
183+ } else {
184+ dtde.dropComplete(false )
185+ }
186+ } catch (e: Exception ) {
187+ LOG .error { " Failed to handle file drop operation: ${e.message} " }
188+ dtde.dropComplete(false )
189+ }
190+ }
191+ }
192+
193+ // Set DropTarget on the browser component and its children
194+ browserInstance.component()?.let { component ->
195+ component.dropTarget = dropTarget
196+ // Also try setting on parent if needed
197+ component.parent?.dropTarget = dropTarget
198+ }
127199
128200 initConnections()
129- connectUi(it )
130- connectApps(it )
201+ connectUi(browserInstance )
202+ connectApps(browserInstance )
131203
132204 loadingPanel.stopLoading()
133205 }
@@ -211,6 +283,36 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di
211283 }
212284 }
213285
286+ private fun validateImageFile (file : File , allowedTypes : Set <String >, maxFileSize : Double , maxDimension : Int ): String? {
287+ val fileName = file.name
288+ val ext = fileName.substringAfterLast(' .' , " " ).lowercase()
289+
290+ if (ext !in allowedTypes) {
291+ return " $fileName : File must be an image in JPEG, PNG, GIF, or WebP format."
292+ }
293+
294+ if (file.length() > maxFileSize) {
295+ return " $fileName : Image must be no more than 3.75MB in size."
296+ }
297+
298+ return try {
299+ val img = read(file)
300+ when {
301+ img == null -> " $fileName : File could not be read as an image."
302+ img.width > maxDimension -> " $fileName : Image must be no more than 8,000px in width."
303+ img.height > maxDimension -> " $fileName : Image must be no more than 8,000px in height."
304+ else -> null
305+ }
306+ } catch (e: Exception ) {
307+ " $fileName : File could not be read as an image."
308+ }
309+ }
310+
311+ companion object {
312+ private val LOG = getLogger<AmazonQPanel >()
313+ private val OBJECT_MAPPER = jacksonObjectMapper()
314+ }
315+
214316 override fun dispose () {
215317 }
216318}
0 commit comments