[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
master 03f5a06a052: Implement multi-window drag-and-drop under Android
From: |
Po Lu |
Subject: |
master 03f5a06a052: Implement multi-window drag-and-drop under Android |
Date: |
Fri, 13 Oct 2023 22:15:58 -0400 (EDT) |
branch: master
commit 03f5a06a052ee0b4b8b77b4460ead717b87c4798
Author: Po Lu <luangruo@yahoo.com>
Commit: Po Lu <luangruo@yahoo.com>
Implement multi-window drag-and-drop under Android
* java/org/gnu/emacs/EmacsNative.java (sendDndDrag, sendDndUri)
(sendDndText): Declare new event-sending functions.
* java/org/gnu/emacs/EmacsView.java (onDragEvent): New function.
* java/org/gnu/emacs/EmacsWindow.java (onDragEvent): New
function; respond to each drag and drop event, request
permissions if necessary and transfer dropped data to Lisp.
* lisp/dnd.el (dnd-unescape-file-uris): New variable.
(dnd-get-local-file-name): If that variable is nil, refrain from
unescaping URLs provided.
* lisp/term/android-win.el (android-handle-dnd-event): New
function.
(special-event-map): Bind drag-n-drop-event.
* src/android.c (sendDndDrag, sendDndUri, sendDndText): New
functions.
* src/androidgui.h (enum android_event_type): New event types
ANDROID_DND_DRAG_EVENT, ANDROID_DND_URI_EVENT,
ANDROID_DND_TEXT_EVENT.
(struct android_dnd_event): New structure.
(union android_event) <dnd>: New field.
* src/androidterm.c (handle_one_android_event)
<ANDROID_DND_..._EVENT>: Generate drag-n-drop events for each
of these types.
(syms_of_androidterm) <Quri, Qtext>: New defsyms.
---
java/org/gnu/emacs/EmacsNative.java | 11 +++
java/org/gnu/emacs/EmacsView.java | 14 ++++
java/org/gnu/emacs/EmacsWindow.java | 138 +++++++++++++++++++++++++++++++++++-
lisp/dnd.el | 10 ++-
lisp/term/android-win.el | 58 +++++++++++++++
src/android.c | 94 ++++++++++++++++++++++++
src/androidgui.h | 30 ++++++++
src/androidterm.c | 43 +++++++++++
8 files changed, 394 insertions(+), 4 deletions(-)
diff --git a/java/org/gnu/emacs/EmacsNative.java
b/java/org/gnu/emacs/EmacsNative.java
index d8524d92130..7d7e1e5d831 100644
--- a/java/org/gnu/emacs/EmacsNative.java
+++ b/java/org/gnu/emacs/EmacsNative.java
@@ -175,6 +175,17 @@ public final class EmacsNative
public static native long sendExpose (short window, int x, int y,
int width, int height);
+ /* Send an ANDROID_DND_DRAG event. */
+ public static native long sendDndDrag (short window, int x, int y);
+
+ /* Send an ANDROID_DND_URI event. */
+ public static native long sendDndUri (short window, int x, int y,
+ String text);
+
+ /* Send an ANDROID_DND_TEXT event. */
+ public static native long sendDndText (short window, int x, int y,
+ String text);
+
/* Return the file name associated with the specified file
descriptor, or NULL if there is none. */
public static native byte[] getProcName (int fd);
diff --git a/java/org/gnu/emacs/EmacsView.java
b/java/org/gnu/emacs/EmacsView.java
index 877b1ce2429..2d53231fbf9 100644
--- a/java/org/gnu/emacs/EmacsView.java
+++ b/java/org/gnu/emacs/EmacsView.java
@@ -24,6 +24,7 @@ import android.content.Context;
import android.text.InputType;
import android.view.ContextMenu;
+import android.view.DragEvent;
import android.view.View;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -566,6 +567,19 @@ public final class EmacsView extends ViewGroup
return window.onTouchEvent (motion);
}
+ @Override
+ public boolean
+ onDragEvent (DragEvent drag)
+ {
+ /* Inter-program drag and drop isn't supported under Android 23
+ and earlier. */
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
+ return false;
+
+ return window.onDragEvent (drag);
+ }
+
private void
diff --git a/java/org/gnu/emacs/EmacsWindow.java
b/java/org/gnu/emacs/EmacsWindow.java
index 8d444aa27f5..3d2d86624a7 100644
--- a/java/org/gnu/emacs/EmacsWindow.java
+++ b/java/org/gnu/emacs/EmacsWindow.java
@@ -27,6 +27,8 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
+import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.Context;
import android.graphics.Rect;
@@ -34,12 +36,15 @@ import android.graphics.Canvas;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
-import android.view.View;
-import android.view.ViewManager;
+import android.net.Uri;
+
+import android.view.DragEvent;
import android.view.Gravity;
+import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
-import android.view.InputDevice;
+import android.view.View;
+import android.view.ViewManager;
import android.view.WindowManager;
import android.util.Log;
@@ -1560,4 +1565,131 @@ public final class EmacsWindow extends EmacsHandleObject
rect.width (), rect.height ());
}
}
+
+
+
+ /* Drag and drop.
+
+ Android 7.0 and later permit multiple windows to be juxtaposed
+ on-screen, consequently enabling items selected from one window
+ to be dragged onto another. Data is transferred across program
+ boundaries using ClipData items, much the same way clipboard data
+ is transferred.
+
+ When an item is dropped, Emacs must ascertain whether the clip
+ data represents plain text, a content URI incorporating a file,
+ or some other data. This is implemented by examining the clip
+ data's ``description'', which enumerates each of the MIME data
+ types the clip data is capable of providing data in.
+
+ If the clip data represents plain text, then that text is copied
+ into a string and conveyed to Lisp code. Otherwise, Emacs must
+ solicit rights to access the URI from the system, absent which it
+ is accounted plain text and reinterpreted as such, to cue the
+ user that something has gone awry.
+
+ Moreover, events are regularly sent as the item being dragged
+ travels across the frame, even if it might not be dropped. This
+ facilitates cursor motion and scrolling in response, as provided
+ by the options dnd-indicate-insertion-point and
+ dnd-scroll-margin. */
+
+ /* Register the drag and drop event EVENT. */
+
+ public boolean
+ onDragEvent (DragEvent event)
+ {
+ ClipData data;
+ ClipDescription description;
+ int i, x, y;
+ String type;
+ Uri uri;
+ EmacsActivity activity;
+
+ x = (int) event.getX ();
+ y = (int) event.getY ();
+
+ switch (event.getAction ())
+ {
+ case DragEvent.ACTION_DRAG_STARTED:
+ /* Return true to continue the drag and drop operation. */
+ return true;
+
+ case DragEvent.ACTION_DRAG_LOCATION:
+ /* Send this drag motion event to Emacs. */
+ EmacsNative.sendDndDrag (handle, x, y);
+ return true;
+
+ case DragEvent.ACTION_DROP:
+ /* Judge whether this is plain text, or if it's a file URI for
+ which permissions must be requested. */
+
+ data = event.getClipData ();
+ description = data.getDescription ();
+
+ /* If there are insufficient items within the clip data,
+ return false. */
+
+ if (data.getItemCount () < 1)
+ return false;
+
+ /* Search for plain text data within the clipboard. */
+
+ for (i = 0; i < description.getMimeTypeCount (); ++i)
+ {
+ type = description.getMimeType (i);
+
+ if (type.equals (ClipDescription.MIMETYPE_TEXT_PLAIN)
+ || type.equals (ClipDescription.MIMETYPE_TEXT_HTML))
+ {
+ /* The data being dropped is plain text; encode it
+ suitably and send it to the main thread. */
+ type = (data.getItemAt (0).coerceToText (EmacsService.SERVICE)
+ .toString ());
+ EmacsNative.sendDndText (handle, x, y, type);
+ return true;
+ }
+ else if (type.equals (ClipDescription.MIMETYPE_TEXT_URILIST))
+ {
+ /* The data being dropped is a list of URIs; encode it
+ suitably and send it to the main thread. */
+ type = (data.getItemAt (0).coerceToText (EmacsService.SERVICE)
+ .toString ());
+ EmacsNative.sendDndUri (handle, x, y, type);
+ return true;
+ }
+ else
+ {
+ /* If the item dropped is a URI, send it to the main
+ thread. */
+ uri = data.getItemAt (0).getUri ();
+
+ /* Attempt to acquire permissions for this URI;
+ failing which, insert it as text instead. */
+
+ if (uri.getScheme () != null
+ && uri.getScheme ().equals ("content")
+ && (activity = EmacsActivity.lastFocusedActivity) != null)
+ {
+ if (activity.requestDragAndDropPermissions (event) == null)
+ uri = null;
+ }
+
+ if (uri != null)
+ EmacsNative.sendDndUri (handle, x, y, uri.toString ());
+ else
+ {
+ type = (data.getItemAt (0)
+ .coerceToText (EmacsService.SERVICE)
+ .toString ());
+ EmacsNative.sendDndText (handle, x, y, type);
+ }
+
+ return true;
+ }
+ }
+ }
+
+ return true;
+ }
};
diff --git a/lisp/dnd.el b/lisp/dnd.el
index 67907ec403e..14581e3d414 100644
--- a/lisp/dnd.el
+++ b/lisp/dnd.el
@@ -201,6 +201,11 @@ Return nil if URI is not a local file."
(string-equal sysname-no-dot hostname)))
(concat "file://" (substring uri (+ 7 (length hostname))))))))
+(defvar dnd-unescape-file-uris t
+ "Whether to unescape file: URIs before they are opened.
+Bind this to nil when providing `dnd-get-local-file-name' with a
+file name that may incorporate URI escape sequences.")
+
(defun dnd--unescape-uri (uri)
;; Merge with corresponding code in URL library.
(replace-regexp-in-string
@@ -226,7 +231,10 @@ Return nil if URI is not a local file."
'utf-8
(or file-name-coding-system
default-file-name-coding-system))))
- (and f (setq f (decode-coding-string (dnd--unescape-uri f) coding)))
+ (and f (setq f (decode-coding-string
+ (if dnd-unescape-file-uris
+ (dnd--unescape-uri f) f)
+ coding)))
(when (and f must-exist (not (file-readable-p f)))
(setq f nil))
f))
diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el
index db873c176c8..f3f5c227df0 100644
--- a/lisp/term/android-win.el
+++ b/lisp/term/android-win.el
@@ -233,5 +233,63 @@ EVENT is a preedit-text event."
(defconst x-pointer-invisible 0)
+;; Drag-and-drop. There are two formats of drag and drop event under
+;; Android. The data field of the first is set to a cons of X and Y,
+;; which represent a position within a frame that something is being
+;; dragged over, whereas that of the second is a cons of either symbol
+;; `uri' or `text' and a list of URIs or text to insert.
+;;
+;; If a content:// URI is encountered, then it in turn designates a
+;; file within the special-purpose /content/by-authority directory,
+;; which facilitates accessing such atypical files.
+
+(declare-function url-type "url-parse")
+(declare-function url-host "url-parse")
+(declare-function url-filename "url-parse")
+
+(defun android-handle-dnd-event (event)
+ "Respond to a drag-and-drop event EVENT.
+If it reflects the motion of an item above a frame, call
+`dnd-handle-movement' to move the cursor or scroll the window
+under the item pursuant to the pertinent user options.
+
+If it reflects dropped text, insert such text within window at
+the location of the drop.
+
+If it reflects a list of URIs, then open each URI, converting
+content:// URIs into the special file names which represent them."
+ (interactive "e")
+ (let ((message (caddr event))
+ (posn (event-start event)))
+ (cond ((fixnump (car message))
+ (dnd-handle-movement posn))
+ ((eq (car message) 'text)
+ (let ((window (posn-window posn)))
+ (with-selected-window window
+ (unless mouse-yank-at-point
+ (goto-char (posn-point (event-start event))))
+ (dnd-insert-text window 'copy (cdr message)))))
+ ((eq (car message) 'uri)
+ (let ((uri-list (split-string (cdr message)
+ "[\0\r\n]" t))
+ (dnd-unescape-file-uris t))
+ (dolist (uri uri-list)
+ (ignore-errors
+ (let ((url (url-generic-parse-url uri)))
+ (when (equal (url-type url) "content")
+ ;; Replace URI with a matching /content file
+ ;; name.
+ (setq uri (format "file:/content/by-authority/%s%s"
+ (url-host url)
+ (url-filename url))
+ ;; And guarantee that this file URI is not
+ ;; subject to URI decoding, for it must be
+ ;; transformed back into a content URI.
+ dnd-unescape-file-uris nil))))
+ (dnd-handle-one-url (posn-window posn) 'copy uri)))))))
+
+(define-key special-event-map [drag-n-drop] 'android-handle-dnd-event)
+
+
(provide 'android-win)
;; android-win.el ends here.
diff --git a/src/android.c b/src/android.c
index fa7bfe6c0f0..8c4748cccf6 100644
--- a/src/android.c
+++ b/src/android.c
@@ -2319,6 +2319,100 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object,
return event_serial;
}
+JNIEXPORT jboolean JNICALL
+NATIVE_NAME (sendDndDrag) (JNIEnv *env, jobject object,
+ jshort window, jint x, jint y)
+{
+ JNI_STACK_ALIGNMENT_PROLOGUE;
+
+ union android_event event;
+
+ event.dnd.type = ANDROID_DND_DRAG_EVENT;
+ event.dnd.serial = ++event_serial;
+ event.dnd.window = window;
+ event.dnd.x = x;
+ event.dnd.y = y;
+ event.dnd.uri_or_string = NULL;
+ event.dnd.length = 0;
+
+ android_write_event (&event);
+ return event_serial;
+}
+
+JNIEXPORT jboolean JNICALL
+NATIVE_NAME (sendDndUri) (JNIEnv *env, jobject object,
+ jshort window, jint x, jint y,
+ jstring string)
+{
+ JNI_STACK_ALIGNMENT_PROLOGUE;
+
+ union android_event event;
+ const jchar *characters;
+ jsize length;
+ uint16_t *buffer;
+
+ event.dnd.type = ANDROID_DND_URI_EVENT;
+ event.dnd.serial = ++event_serial;
+ event.dnd.window = window;
+ event.dnd.x = x;
+ event.dnd.y = y;
+
+ length = (*env)->GetStringLength (env, string);
+ buffer = malloc (length * sizeof *buffer);
+ characters = (*env)->GetStringChars (env, string, NULL);
+
+ if (!characters)
+ /* The JVM has run out of memory; return and let the out of memory
+ error take its course. */
+ return 0;
+
+ memcpy (buffer, characters, length * sizeof *buffer);
+ (*env)->ReleaseStringChars (env, string, characters);
+
+ event.dnd.uri_or_string = buffer;
+ event.dnd.length = length;
+
+ android_write_event (&event);
+ return event_serial;
+}
+
+JNIEXPORT jboolean JNICALL
+NATIVE_NAME (sendDndText) (JNIEnv *env, jobject object,
+ jshort window, jint x, jint y,
+ jstring string)
+{
+ JNI_STACK_ALIGNMENT_PROLOGUE;
+
+ union android_event event;
+ const jchar *characters;
+ jsize length;
+ uint16_t *buffer;
+
+ event.dnd.type = ANDROID_DND_TEXT_EVENT;
+ event.dnd.serial = ++event_serial;
+ event.dnd.window = window;
+ event.dnd.x = x;
+ event.dnd.y = y;
+
+ length = (*env)->GetStringLength (env, string);
+ buffer = malloc (length * sizeof *buffer);
+ characters = (*env)->GetStringChars (env, string, NULL);
+
+ if (!characters)
+ /* The JVM has run out of memory; return and let the out of memory
+ error take its course. */
+ return 0;
+
+ memcpy (buffer, characters, length * sizeof *buffer);
+ (*env)->ReleaseStringChars (env, string, characters);
+
+ event.dnd.uri_or_string = buffer;
+ event.dnd.length = length;
+
+ android_write_event (&event);
+ return event_serial;
+}
+
JNIEXPORT jboolean JNICALL
NATIVE_NAME (shouldForwardMultimediaButtons) (JNIEnv *env,
jobject object)
diff --git a/src/androidgui.h b/src/androidgui.h
index b58c39a5276..5fab5023ba4 100644
--- a/src/androidgui.h
+++ b/src/androidgui.h
@@ -248,6 +248,9 @@ enum android_event_type
ANDROID_CONTEXT_MENU,
ANDROID_EXPOSE,
ANDROID_INPUT_METHOD,
+ ANDROID_DND_DRAG_EVENT,
+ ANDROID_DND_URI_EVENT,
+ ANDROID_DND_TEXT_EVENT,
};
struct android_any_event
@@ -510,6 +513,28 @@ struct android_ime_event
unsigned long counter;
};
+struct android_dnd_event
+{
+ /* Type of the event. */
+ enum android_event_type type;
+
+ /* The event serial. */
+ unsigned long serial;
+
+ /* The window that gave rise to the event. */
+ android_window window;
+
+ /* X and Y coordinates of the event. */
+ int x, y;
+
+ /* Data tied to this event, such as a URI or clipboard string.
+ Must be deallocated with `free'. */
+ unsigned short *uri_or_string;
+
+ /* Length of that data. */
+ size_t length;
+};
+
union android_event
{
enum android_event_type type;
@@ -541,6 +566,11 @@ union android_event
/* This is used to dispatch input method editing requests. */
struct android_ime_event ime;
+
+ /* There is no analog under X because Android defines a strict DND
+ protocol, whereas there exist several competing X protocols
+ implemented in terms of X client messages. */
+ struct android_dnd_event dnd;
};
enum
diff --git a/src/androidterm.c b/src/androidterm.c
index ef3c20f4e0f..9d6517cce2b 100644
--- a/src/androidterm.c
+++ b/src/androidterm.c
@@ -1706,6 +1706,45 @@ handle_one_android_event (struct android_display_info
*dpyinfo,
goto OTHER;
+ case ANDROID_DND_DRAG_EVENT:
+
+ if (!any)
+ goto OTHER;
+
+ /* Generate a drag and drop event to convey its position. */
+ inev.ie.kind = DRAG_N_DROP_EVENT;
+ XSETFRAME (inev.ie.frame_or_window, any);
+ inev.ie.timestamp = ANDROID_CURRENT_TIME;
+ XSETINT (inev.ie.x, event->dnd.x);
+ XSETINT (inev.ie.y, event->dnd.y);
+ inev.ie.arg = Fcons (inev.ie.x, inev.ie.y);
+ goto OTHER;
+
+ case ANDROID_DND_URI_EVENT:
+ case ANDROID_DND_TEXT_EVENT:
+
+ if (!any)
+ {
+ free (event->dnd.uri_or_string);
+ goto OTHER;
+ }
+
+ /* An item was dropped over ANY, and is a file in the form of a
+ content or file URI or a string to be inserted. Generate an
+ event with this information. */
+
+ inev.ie.kind = DRAG_N_DROP_EVENT;
+ XSETFRAME (inev.ie.frame_or_window, any);
+ inev.ie.timestamp = ANDROID_CURRENT_TIME;
+ XSETINT (inev.ie.x, event->dnd.x);
+ XSETINT (inev.ie.y, event->dnd.y);
+ inev.ie.arg = Fcons ((event->type == ANDROID_DND_TEXT_EVENT
+ ? Qtext : Quri),
+ android_decode_utf16 (event->dnd.uri_or_string,
+ event->dnd.length));
+ free (event->dnd.uri_or_string);
+ goto OTHER;
+
default:
goto OTHER;
}
@@ -6593,6 +6632,10 @@ Emacs is running on. */);
pdumper_do_now_and_after_load (android_set_build_fingerprint);
DEFSYM (Qx_underline_at_descent_line, "x-underline-at-descent-line");
+
+ /* Symbols defined for DND events. */
+ DEFSYM (Quri, "uri");
+ DEFSYM (Qtext, "text");
}
void
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- master 03f5a06a052: Implement multi-window drag-and-drop under Android,
Po Lu <=