3
3
4
4
package software.aws.toolkits.jetbrains.services.amazonq.toolwindow
5
5
6
+ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
6
7
import com.intellij.idea.AppMode
7
8
import com.intellij.openapi.Disposable
8
9
import com.intellij.openapi.components.service
@@ -20,6 +21,8 @@ import kotlinx.coroutines.CoroutineScope
20
21
import kotlinx.coroutines.flow.first
21
22
import kotlinx.coroutines.launch
22
23
import kotlinx.coroutines.withContext
24
+ import software.aws.toolkits.core.utils.error
25
+ import software.aws.toolkits.core.utils.getLogger
23
26
import software.aws.toolkits.jetbrains.core.coroutines.EDT
24
27
import software.aws.toolkits.jetbrains.isDeveloperMode
25
28
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
@@ -44,7 +47,12 @@ import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
44
47
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable
45
48
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable
46
49
import 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
47
54
import java.util.concurrent.CompletableFuture
55
+ import javax.imageio.ImageIO.read
48
56
import javax.swing.JButton
49
57
50
58
class AmazonQPanel (val project : Project , private val scope : CoroutineScope ) : Disposable {
@@ -122,12 +130,76 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di
122
130
123
131
withContext(EDT ) {
124
132
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
+ }
127
199
128
200
initConnections()
129
- connectUi(it )
130
- connectApps(it )
201
+ connectUi(browserInstance )
202
+ connectApps(browserInstance )
131
203
132
204
loadingPanel.stopLoading()
133
205
}
@@ -211,6 +283,36 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di
211
283
}
212
284
}
213
285
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
+
214
316
override fun dispose () {
215
317
}
216
318
}
0 commit comments