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.
This commit is contained in:
parent
0dd7e6e3ae
commit
03f5a06a05
8 changed files with 394 additions and 4 deletions
|
@ -175,6 +175,17 @@ public static native long sendContextMenu (short window, int menuEventID,
|
|||
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);
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
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 @@ else if (child.getVisibility () != GONE)
|
|||
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
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
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.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 @@ else if (EmacsWindow.this.isMapped)
|
|||
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;
|
||||
}
|
||||
};
|
||||
|
|
10
lisp/dnd.el
10
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))
|
||||
|
|
|
@ -232,6 +232,64 @@ EVENT is a preedit-text event."
|
|||
(defconst x-pointer-xterm 1008)
|
||||
(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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue