emacs-diffs
[Top][All Lists]
Advanced

[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



reply via email to

[Prev in Thread] Current Thread [Next in Thread]