Bug 1878599 - Allow to insert text on text control from dropping external application on Geckoview. r=masayuki,geckoview-reviewers,owlish

Android's drag and drop API will set a dropped item on `drop` event. But other
platforms will be set during `dragstart` event.

Editor's drag and drop event listener checks current dropped item on some
events such as `dragover` event in
`EditorEventListener::DragEventHasSupportingData`. Since there is no way to
set dropped item on `dragover` event, GeckoView will set temporary dropped
item with MIME type.

Differential Revision: https://phabricator.services.mozilla.com/D200618
This commit is contained in:
Makoto Kato 2024-02-14 00:58:48 +00:00
Родитель 1aa0743176
Коммит dafa6009d9
5 изменённых файлов: 84 добавлений и 20 удалений

Просмотреть файл

@ -22,5 +22,6 @@
<div id="drop" style="border: 1px solid red; width: 200px; height: 100px"> <div id="drop" style="border: 1px solid red; width: 200px; height: 100px">
drop drop
</div> </div>
<textarea id="textarea" style="width: 200px; height: 100px"></textarea>
</body> </body>
</html> </html>

Просмотреть файл

@ -13,6 +13,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress import androidx.test.filters.SdkSuppress
import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.equalTo
import org.json.JSONObject
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
@ -41,12 +42,14 @@ class DragAndDropTest : BaseSessionTest() {
fieldY.set(dragEvent, y) fieldY.set(dragEvent, y)
} }
val clipData = ClipData.newPlainText("label", "foo")
if (action == DragEvent.ACTION_DROP) { if (action == DragEvent.ACTION_DROP) {
val clipData = ClipData.newPlainText("label", "foo")
val fieldClipData = DragEvent::class.java.getDeclaredField("mClipData") val fieldClipData = DragEvent::class.java.getDeclaredField("mClipData")
fieldClipData.setAccessible(true) fieldClipData.setAccessible(true)
fieldClipData.set(dragEvent, clipData) fieldClipData.set(dragEvent, clipData)
}
if (action != DragEvent.ACTION_DRAG_ENDED) {
var clipDescription = clipData.getDescription() var clipDescription = clipData.getDescription()
val fieldClipDescription = DragEvent::class.java.getDeclaredField("mClipDescription") val fieldClipDescription = DragEvent::class.java.getDeclaredField("mClipDescription")
fieldClipDescription.setAccessible(true) fieldClipDescription.setAccessible(true)
@ -56,6 +59,22 @@ class DragAndDropTest : BaseSessionTest() {
return dragEvent return dragEvent
} }
fun sendDragEvent(startX: Float, startY: Float, endY: Float) {
// Android doesn't fire MotionEvent during drag and drop.
val dragStartEvent = createDragEvent(DragEvent.ACTION_DRAG_STARTED)
mainSession.panZoomController.onDragEvent(dragStartEvent)
val dragEnteredEvent = createDragEvent(DragEvent.ACTION_DRAG_ENTERED)
mainSession.panZoomController.onDragEvent(dragEnteredEvent)
listOf(startY, endY).forEach {
val dragLocationEvent = createDragEvent(DragEvent.ACTION_DRAG_LOCATION, startX, it)
mainSession.panZoomController.onDragEvent(dragLocationEvent)
}
val dropEvent = createDragEvent(DragEvent.ACTION_DROP, startX, endY)
mainSession.panZoomController.onDragEvent(dropEvent)
val dragEndedEvent = createDragEvent(DragEvent.ACTION_DRAG_ENDED)
mainSession.panZoomController.onDragEvent(dragEndedEvent)
}
@WithDisplay(width = 300, height = 300) @WithDisplay(width = 300, height = 300)
@Test @Test
fun dragStartTest() { fun dragStartTest() {
@ -94,20 +113,42 @@ class DragAndDropTest : BaseSessionTest() {
""".trimIndent(), """.trimIndent(),
) )
// Android doesn't fire MotionEvent during drag and drop. sendDragEvent(100.0F, 150.0F, 250.0F)
val dragStartEvent = createDragEvent(DragEvent.ACTION_DRAG_STARTED)
mainSession.panZoomController.onDragEvent(dragStartEvent)
val dragEnteredEvent = createDragEvent(DragEvent.ACTION_DRAG_ENTERED)
mainSession.panZoomController.onDragEvent(dragEnteredEvent)
listOf(150.0F, 250.0F).forEach {
val dragLocationEvent = createDragEvent(DragEvent.ACTION_DRAG_LOCATION, 100.0F, it)
mainSession.panZoomController.onDragEvent(dragLocationEvent)
}
val dropEvent = createDragEvent(DragEvent.ACTION_DROP, 100.0F, 250.0F)
mainSession.panZoomController.onDragEvent(dropEvent)
val dragEndedEvent = createDragEvent(DragEvent.ACTION_DRAG_ENDED)
mainSession.panZoomController.onDragEvent(dragEndedEvent)
assertThat("drop event is fired correctly", promise.value as String, equalTo("foo")) assertThat("drop event is fired correctly", promise.value as String, equalTo("foo"))
} }
@WithDisplay(width = 300, height = 500)
@Test
fun dropFromExternalToTextControlTest() {
mainSession.loadTestPath(DND_HTML_PATH)
sessionRule.waitForPageStop()
val promiseDragOver = mainSession.evaluatePromiseJS(
"""
new Promise(
r => document.querySelector('textarea').addEventListener(
'dragover',
e => r({ types: e.dataTransfer.types, data: e.dataTransfer.getData('text/plain') }),
{ once: true }))
""".trimIndent(),
)
val promiseSetValue = mainSession.evaluatePromiseJS(
"""
new Promise(
r => document.querySelector('textarea').addEventListener(
'input',
e => r(document.querySelector('textarea').value),
{ once: true }))
""".trimIndent(),
)
sendDragEvent(100.0F, 250.0F, 450.0F)
var value = promiseDragOver.value as JSONObject
assertThat("dataTransfer type is text/plain", value.getJSONArray("types").getString(0), equalTo("text/plain"))
assertThat("dataTransfer set empty string during dragover event", value.getString("data"), equalTo(""))
assertThat("input event is fired correctly", promiseSetValue.value as String, equalTo("foo"))
}
} }

Просмотреть файл

@ -29,6 +29,10 @@ public class GeckoDragAndDrop {
/** The drag/drop data is nsITransferable and stored into nsDragService. */ /** The drag/drop data is nsITransferable and stored into nsDragService. */
private static final String MIMETYPE_NATIVE = "application/x-moz-draganddrop"; private static final String MIMETYPE_NATIVE = "application/x-moz-draganddrop";
private static final String[] sSupportedMimeType = {
MIMETYPE_NATIVE, ClipDescription.MIMETYPE_TEXT_HTML, ClipDescription.MIMETYPE_TEXT_PLAIN
};
private static ClipData sDragClipData; private static ClipData sDragClipData;
private static float sX; private static float sX;
private static float sY; private static float sY;
@ -71,6 +75,12 @@ public class GeckoDragAndDrop {
this.text = null; this.text = null;
} }
@WrapForJNI(skip = true)
public DropData(final String mimeType) {
this.mimeType = mimeType;
this.text = "";
}
@WrapForJNI(skip = true) @WrapForJNI(skip = true)
public DropData(final String mimeType, final String text) { public DropData(final String mimeType, final String text) {
this.mimeType = mimeType; this.mimeType = mimeType;
@ -133,7 +143,7 @@ public class GeckoDragAndDrop {
} }
/** /**
* Create drop data by DragEvent.ACTION_DROP. This ClipData will be stored into nsDragService as * Create drop data by DragEvent. This ClipData will be stored into nsDragService as
* nsITransferable. If this type has MIMETYPE_NATIVE, this is already stored into nsDragService. * nsITransferable. If this type has MIMETYPE_NATIVE, this is already stored into nsDragService.
* So do nothing. * So do nothing.
* *
@ -141,15 +151,27 @@ public class GeckoDragAndDrop {
* @return DropData that is from ClipData. If null, no data that we can convert to Gecko's type. * @return DropData that is from ClipData. If null, no data that we can convert to Gecko's type.
*/ */
public static DropData createDropData(final DragEvent event) { public static DropData createDropData(final DragEvent event) {
final ClipDescription description = event.getClipDescription();
if (event.getAction() == DragEvent.ACTION_DRAG_ENTERED) {
// Android API cannot get real dragging item until drop event. So we set MIME type only.
for (final String mimeType : sSupportedMimeType) {
if (description.hasMimeType(mimeType)) {
return new DropData(mimeType);
}
}
return null;
}
if (event.getAction() != DragEvent.ACTION_DROP) { if (event.getAction() != DragEvent.ACTION_DROP) {
return null; return null;
} }
final ClipData clip = event.getClipData(); final ClipData clip = event.getClipData();
if (clip == null || clip.getItemCount() == 0) { if (clip == null || clip.getItemCount() == 0) {
return null; return null;
} }
final ClipDescription description = event.getClipDescription();
if (description.hasMimeType(MIMETYPE_NATIVE)) { if (description.hasMimeType(MIMETYPE_NATIVE)) {
if (DEBUG) { if (DEBUG) {
Log.d(LOGTAG, "Drop data is native nsITransferable. Do nothing"); Log.d(LOGTAG, "Drop data is native nsITransferable. Do nothing");

Просмотреть файл

@ -257,10 +257,6 @@ void nsDragService::SetDropData(
} }
nsString buffer(aDropData->Text()->ToString()); nsString buffer(aDropData->Text()->ToString());
if (buffer.IsEmpty()) {
dragService->SetData(nullptr);
return;
}
nsCOMPtr<nsISupports> wrapper; nsCOMPtr<nsISupports> wrapper;
nsPrimitiveHelpers::CreatePrimitiveForData( nsPrimitiveHelpers::CreatePrimitiveForData(
mime, buffer.get(), buffer.Length() * 2, getter_AddRefs(wrapper)); mime, buffer.get(), buffer.Length() * 2, getter_AddRefs(wrapper));

Просмотреть файл

@ -2636,6 +2636,10 @@ void nsWindow::OnDragEvent(int32_t aAction, int64_t aTime, float aX, float aY,
if (message == eDragEnter) { if (message == eDragEnter) {
dragService->StartDragSession(); dragService->StartDragSession();
// For compatibility, we have to set temporary data.
auto dropData =
mozilla::java::GeckoDragAndDrop::DropData::Ref::From(aDropData);
nsDragService::SetDropData(dropData);
} }
nsCOMPtr<nsIDragSession> dragSession; nsCOMPtr<nsIDragSession> dragSession;