Enable DND handlers to receive more than one URI at a time

* doc/lispref/frames.texi (Drag and Drop): Illustrate the effect
of the dnd-multiple-handler property and how convergent handlers
are reconciled.

* etc/NEWS (Lisp Changes in Emacs 30.1): Announce this change.

* lisp/dnd.el (dnd-protocol-alist): Bring doc string up to date.
(dnd-handle-one-url): Obsolete this function.
(dnd-handle-multiple-urls): New function.

* lisp/pgtk-dnd.el (pgtk-dnd-handle-uri-list)
(pgtk-dnd-handle-file-name):

* lisp/term/android-win.el (android-handle-dnd-event):

* lisp/term/haiku-win.el (haiku-drag-and-drop):

* lisp/term/ns-win.el (ns-drag-n-drop):

* lisp/term/w32-win.el (w32-handle-dropped-file):

* lisp/x-dnd.el (x-dnd-handle-uri-list, x-dnd-handle-file-name):
Reimplement in terms of `dnd-handle-multiple-uris'.

* lisp/term/pgtk-win.el (pgtk-drag-n-drop)
(pgtk-drag-n-drop-other-frame, pgtk-drag-n-drop-as-text): Efface
detritus that remained after the removal of the old PGTK drag
and drop implementation.

* test/lisp/dnd-tests.el (ert-x, dnd-tests-list-1)
(dnd-tests-list-2, dnd-tests-list-3, dnd-tests-list-4)
(dnd-tests-local-file-function, dnd-tests-remote-file-function)
(dnd-tests-http-scheme-function, dnd-tests-browse-url-handler)
(dnd-tests-receive-multiple-urls): New tests.
This commit is contained in:
Po Lu 2023-10-26 11:37:58 +00:00
parent b62ad00981
commit 11f44ec6dd
11 changed files with 283 additions and 101 deletions

View file

@ -4724,9 +4724,9 @@ seldom consistent medley of data types.
@cindex drag and drop
Data transferred by drag and drop is generally either plain text or
a URL designating a file or another resource. When text is dropped,
it is inserted at the location of the drop, with recourse to saving it
into the kill ring if that is not possible.
a list of URLs designating files or other resources. When text is
dropped, it is inserted at the location of the drop, with recourse to
saving it into the kill ring if that is not possible.
URLs dropped are supplied to pertinent @dfn{DND handler functions}
in the variable @code{dnd-protocol-alist}, or alternatively ``URL
@ -4740,9 +4740,14 @@ This variable is an alist between regexps against which URLs are
matched and DND handler functions called on the dropping of matching
URLs.
Each handler function is called with the URL that matched it and one
of the symbols @code{copy}, @code{move}, @code{link}, @code{private}
or @code{ask} identifying the action to be taken.
@cindex dnd-multiple-handler, a symbol property
If a handler function is a symbol whose @code{dnd-multiple-handler}
property (@pxref{Symbol Properties}) is set, then upon a drop it is
given a list of every URL that matches its regexp; absent this
property, it is called once for each of those URLs. Following this
first argument is one of the symbols @code{copy}, @code{move},
@code{link}, @code{private} or @code{ask} identifying the action to be
taken.
If @var{action} is @code{private}, the program that initiated the drop
does not insist on any particular behavior on the part of its
@ -4750,19 +4755,29 @@ recipient; a reasonable action to take in that case is to open the URL
or copy its contents into the current buffer. The other values of
@var{action} imply much the same as in the @var{action} argument to
@code{dnd-begin-file-drag}.
Once its work completes, a handler function must return a symbol
designating the action it took: either the action it was provided, or
the symbol @code{private}, which communicates to the source of the
drop that the action it prescribed has not been executed.
When multiple handlers match an overlapping subset of items within a
drop, the handler matched against by the greatest number of items is
called to open that subset. The items it is supplied are subsequently
withheld from other handlers, even those they also match.
@end defvar
@cindex drag and drop, X
@cindex drag and drop, other formats
Emacs does not take measures to accept data besides text and URLs by
default, for the window system interfaces which enable this are too
far removed from each other to abstract over consistently. Nor are
DND handlers accorded the capacity to influence the action they are
meant to take, as particular drag-and-drop protocols deny recipients
such control. The X11 drag-and-drop implementation rests on several
underlying protocols that make use of selection transfer and share
much in common, to which low level access is provided through the
following functions and variables:
Emacs does not take measures to accept data besides text and URLs,
for the window system interfaces which enable this are too far removed
from each other to abstract over consistently. Nor are DND handlers
accorded influence over the actions they are meant to take, as
particular drag-and-drop protocols deny recipients such control. The
X11 drag-and-drop implementation rests on several underlying protocols
that make use of selection transfer and share much in common, to which
low level access is provided through the following functions and
variables:
@defvar x-dnd-test-function
This function is called to ascertain whether Emacs should accept a

View file

@ -1122,6 +1122,16 @@ values.
* Lisp Changes in Emacs 30.1
+++
** Drag-and-drop functions can now be called once for compound drops.
It is now possible for drag-and-drop handler functions to respond to
drops incorporating more than one URL. Functions capable of this must
set their 'dnd-multiple-handler' symbol properties to a non-nil value.
See the Info node "(elisp)Drag and Drop".
Incident to this change, the function 'dnd-handle-one-url' has been
made obsolete, for it cannot take these new handlers into account.
** New function 're-disassemble' to see the innards of a regexp.
If you compiled with '--enable-checking', you can use this to help debug
either your regexp performance problems or the regexp engine.

View file

@ -46,13 +46,14 @@
(,(purecopy "^file://") . dnd-open-file) ; URL with host
(,(purecopy "^file:") . dnd-open-local-file) ; Old KDE, Motif, Sun
(,(purecopy "^\\(https?\\|ftp\\|file\\|nfs\\)://") . dnd-open-file))
"The functions to call for different protocols when a drop is made.
This variable is used by `dnd-handle-one-url' and `dnd-handle-file-name'.
This variable is used by `dnd-handle-multiple-urls'.
The list contains of (REGEXP . FUNCTION) pairs.
The functions shall take two arguments, URL, which is the URL dropped and
ACTION which is the action to be performed for the drop (move, copy, link,
private or ask).
If a function's `dnd-multiple-handler' property is set, it is provided
a list of each URI dropped instead.
If no match is found here, and the value of `browse-url-browser-function'
is a pair of (REGEXP . FUNCTION), those regexps are tried for a match.
If no match is found, the URL is inserted as text by calling `dnd-insert-text'.
@ -159,7 +160,10 @@ If no match is found here, `browse-url-handlers' and
`browse-url-default-handlers' are searched for a match.
If no match is found, just call `dnd-insert-text'. WINDOW is
where the drop happened, ACTION is the action for the drop, URL
is what has been dropped. Returns ACTION."
is what has been dropped. Returns ACTION.
This function has been obsolete since Emacs 30.1; it has been
supplanted by `dnd-handle-multiple-urls'."
(let (ret)
(or
(catch 'done
@ -180,6 +184,90 @@ is what has been dropped. Returns ACTION."
(setq ret 'private)))
ret))
(make-obsolete 'dnd-handle-one-url 'dnd-handle-multiple-urls "30.1")
(defun dnd-handle-multiple-urls (window urls action)
"Select a handler for, then open, each element of URLS.
The argument ACTION is the action which must be taken, much as
that to `dnd-begin-file-drag'.
Assign and give each URL to one of the \"DND handler\" functions
listed in the variable `dnd-protocol-alist'. When multiple
handlers matching the same subset of URLs exist, give precedence
to the handler assigned the greatest number of URLs.
If a handler is a symbol with the property
`dnd-multiple-handler', call it with ACTION and a list of every
URL it is assigned. Otherwise, call it once for each URL
assigned with ACTION and the URL in question.
Subsequently open URLs that don't match any handlers opened with
any handler selected by `browse-url-select-handler', and failing
even that, insert them with `dnd-insert-text'.
Return a symbol designating the actions taken by each DND handler
called. If all DND handlers called return the same symbol,
return that symbol; otherwise, or if no DND handlers are called,
return `private'.
Do not rely on the contents of URLS after calling this function,
for it will be modified."
(let ((list nil) (return-value nil))
(with-selected-window window
(dolist (handler dnd-protocol-alist)
(let ((pattern (car handler))
(handler (cdr handler)))
(dolist (uri urls)
(when (string-match pattern uri)
(let ((cell (or (cdr (assq handler list))
(let ((cell (cons handler nil)))
(push cell list)
cell))))
(setcdr cell (cons uri (cdr cell))))))))
(setq list (nreverse list))
;; While unassessed handlers still exist...
(while list
;; Sort list by the number of URLs assigned to each handler.
(setq list (sort list (lambda (first second)
(> (length (cdr first))
(length (cdr second))))))
;; Call the handler in its car before removing each URL from
;; URLs.
(let ((handler (caar list))
(entry-urls (cdar list)))
(setq list (cdr list))
(when entry-urls
(if (and (symbolp handler)
(get handler 'dnd-multiple-handler))
(progn
(let ((value (funcall handler entry-urls action)))
(if (or (not return-value)
(eq return-value value))
(setq return-value value)
(setq return-value 'private)))
(dolist (url entry-urls)
(setq urls (delq url urls))
;; And each handler-URL list after this.
(dolist (item list)
(setcdr item (delq url (cdr item))))))
(dolist (url entry-urls)
(let ((value (funcall handler url action)))
(if (or (not return-value) (eq return-value value))
(setq return-value value)
(setq return-value 'private)))
(setq urls (delq url urls))
;; And each handler-URL list after this.
(dolist (item list)
(setcdr item (delq url (cdr item)))))))))
;; URLS should now incorporate only those which haven't been
;; assigned their own handlers.
(dolist (leftover urls)
(setq return-value 'private)
(if-let ((handler (browse-url-select-handler leftover
'internal)))
(funcall handler leftover action)
(dnd-insert-text window action leftover)))
(or return-value 'private))))
(defun dnd-get-local-file-uri (uri)
"Return an uri converted to file:/// syntax if uri is a local file.

View file

@ -238,10 +238,9 @@ WINDOW is the window where the drop happened.
STRING is the uri-list as a string. The URIs are separated by \\r\\n."
(let ((uri-list (split-string string "[\0\r\n]" t))
retval)
(dolist (bf uri-list)
;; If one URL is handled, treat as if the whole drop succeeded.
(let ((did-action (dnd-handle-one-url window action bf)))
(when did-action (setq retval did-action))))
(let ((did-action (dnd-handle-multiple-urls window uri-list
action)))
(when did-action (setq retval did-action)))
retval))
(defun pgtk-dnd-handle-file-name (window action string)
@ -252,17 +251,21 @@ STRING is the file names as a string, separated by nulls."
(coding (or file-name-coding-system
default-file-name-coding-system))
retval)
(dolist (bf uri-list)
;; If one URL is handled, treat as if the whole drop succeeded.
(if coding (setq bf (encode-coding-string bf coding)))
(let* ((file-uri (concat "file://"
(mapconcat 'url-hexify-string
(split-string bf "/") "/")))
(did-action (dnd-handle-one-url window action file-uri)))
(when did-action (setq retval did-action))))
(let ((did-action
(dnd-handle-multiple-urls
window action (mapcar
(lambda (item)
(when coding
(setq item (encode-coding-string item
coding)))
(concat "file://"
(mapconcat 'url-hexify-string
(split-string item "/")
"/")))
uri-list))))
(when did-action (setq retval did-action)))
retval))
(defun pgtk-dnd-choose-type (types &optional known-types)
"Choose which type we want to receive for the drop.
TYPES are the types the source of the drop offers, a vector of type names

View file

@ -272,6 +272,7 @@ content:// URIs into the special file names which represent them."
((eq (car message) 'uri)
(let ((uri-list (split-string (cdr message)
"[\0\r\n]" t))
(new-uri-list nil)
(dnd-unescape-file-uris t))
(dolist (uri uri-list)
(ignore-errors
@ -286,7 +287,9 @@ content:// URIs into the special file names which represent them."
;; 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)))))))
(push uri new-uri-list))
(dnd-handle-multiple-urls (posn-window posn) 'copy
new-uri-list))))))
(define-key special-event-map [drag-n-drop] 'android-handle-dnd-event)

View file

@ -369,14 +369,15 @@ or a pair of markers) and turns it into a file system reference."
((posn-area (event-start event)))
((assoc "refs" string)
(with-selected-window window
(dolist (filename (cddr (assoc "refs" string)))
(dnd-handle-one-url window action
(concat "file:" filename)))))
(dnd-handle-multiple-urls
window (mapcar
(lambda (name) (concat "file:" name))
(cddr (assoc "refs" string)))
action)))
((assoc "text/uri-list" string)
(dolist (text (cddr (assoc "text/uri-list" string)))
(let ((uri-list (split-string text "[\0\r\n]" t)))
(dolist (bf uri-list)
(dnd-handle-one-url window action bf)))))
(dnd-handle-multiple-urls window uri-list action))))
((assoc "text/plain" string)
(with-selected-window window
(dolist (text (cddr (assoc "text/plain" string)))

View file

@ -520,11 +520,12 @@ string dropped into the current buffer."
(goto-char (posn-point (event-start event)))
(cond ((or (memq 'ns-drag-operation-generic operations)
(memq 'ns-drag-operation-copy operations))
;; Perform the default/copy action.
(dolist (data objects)
(dnd-handle-one-url window 'private (if (eq type 'file)
(concat "file:" data)
data))))
(let ((urls (if (eq type 'file) (mapcar
(lambda (file)
(concat "file:" file))
objects)
objects)))
(dnd-handle-multiple-urls window urls 'private)))
(t
;; Insert the text as is.
(dnd-insert-text window 'private string))))))

View file

@ -48,45 +48,6 @@
(declare-function pgtk-use-im-context "pgtkim.c")
(defun pgtk-drag-n-drop (event &optional new-frame force-text)
"Edit the files listed in the drag-n-drop EVENT.
Switch to a buffer editing the last file dropped."
(interactive "e")
(let* ((window (posn-window (event-start event)))
(arg (car (cdr (cdr event))))
(type (car arg))
(data (car (cdr arg)))
(url-or-string (cond ((eq type 'file)
(concat "file:" data))
(t data))))
(set-frame-selected-window nil window)
(when new-frame
(select-frame (make-frame)))
(raise-frame)
(setq window (selected-window))
(if force-text
(dnd-insert-text window 'private data)
(dnd-handle-one-url window 'private url-or-string))))
(defun pgtk-drag-n-drop-other-frame (event)
"Edit the files listed in the drag-n-drop EVENT, in other frames.
May create new frames, or reuse existing ones. The frame editing
the last file dropped is selected."
(interactive "e")
(pgtk-drag-n-drop event t))
(defun pgtk-drag-n-drop-as-text (event)
"Drop the data in EVENT as text."
(interactive "e")
(pgtk-drag-n-drop event nil t))
(defun pgtk-drag-n-drop-as-text-other-frame (event)
"Drop the data in EVENT as text in a new frame."
(interactive "e")
(pgtk-drag-n-drop event t t))
(global-set-key [drag-n-drop] 'pgtk-drag-n-drop)
(defun pgtk-suspend-error ()
"Don't allow suspending if any of the frames are PGTK frames."
(if (memq 'pgtk (mapcar 'window-system (frame-list)))
@ -392,7 +353,6 @@ Users should not call this function; see `device-class' instead."
(defvaralias 'x-gtk-use-system-tooltips 'use-system-tooltips)
(define-key special-event-map [drag-n-drop] #'pgtk-dnd-handle-drag-n-drop-event)
(add-hook 'after-make-frame-functions #'pgtk-dnd-init-frame)

View file

@ -117,12 +117,14 @@
(split-string (encode-coding-string f coding)
"/")
"/")))
(dnd-handle-one-url window 'private
(concat
(if (eq system-type 'cygwin)
"file://"
"file:")
file-name)))
;; FIXME: is the W32 build capable only of receiving a single file
;; from each drop?
(dnd-handle-multiple-urls window (list (concat
(if (eq system-type 'cygwin)
"file://"
"file:")
file-name))
'private))
(defun w32-drag-n-drop (event &optional new-frame)
"Edit the files listed in the drag-n-drop EVENT.

View file

@ -369,10 +369,9 @@ WINDOW is the window where the drop happened.
STRING is the uri-list as a string. The URIs are separated by \\r\\n."
(let ((uri-list (split-string string "[\0\r\n]" t))
retval)
(dolist (bf uri-list)
;; If one URL is handled, treat as if the whole drop succeeded.
(let ((did-action (dnd-handle-one-url window action bf)))
(when did-action (setq retval did-action))))
(let ((did-action (dnd-handle-multiple-urls window uri-list
action)))
(when did-action (setq retval did-action)))
retval))
(defun x-dnd-handle-file-name (window action string)
@ -383,17 +382,21 @@ STRING is the file names as a string, separated by nulls."
(coding (or file-name-coding-system
default-file-name-coding-system))
retval)
(dolist (bf uri-list)
;; If one URL is handled, treat as if the whole drop succeeded.
(if coding (setq bf (encode-coding-string bf coding)))
(let* ((file-uri (concat "file://"
(mapconcat 'url-hexify-string
(split-string bf "/") "/")))
(did-action (dnd-handle-one-url window action file-uri)))
(when did-action (setq retval did-action))))
(let ((did-action
(dnd-handle-multiple-urls
window action (mapcar
(lambda (item)
(when coding
(setq item (encode-coding-string item
coding)))
(concat "file://"
(mapconcat 'url-hexify-string
(split-string item "/")
"/")))
uri-list))))
(when did-action (setq retval did-action)))
retval))
(defun x-dnd-choose-type (types &optional known-types)
"Choose which type we want to receive for the drop.
TYPES are the types the source of the drop offers, a vector of type names

View file

@ -33,6 +33,7 @@
(require 'tramp)
(require 'select)
(require 'ert-x)
(require 'browse-url)
(defvar dnd-tests-selection-table nil
"Alist of selection names to their values.")
@ -437,5 +438,100 @@ This function only tries to handle strings."
(ignore-errors
(delete-file normal-temp-file)))))
(defvar dnd-tests-list-1 '("file:///usr/openwin/include/pixrect/pr_impl.h"
"file:///usr/openwin/include/pixrect/pr_io.h")
"Sample data for tests concerning the treatment of drag-and-drop URLs.")
(defvar dnd-tests-list-2 '("file:///usr/openwin/include/pixrect/pr_impl.h"
"file://remote/usr/openwin/include/pixrect/pr_io.h")
"Sample data for tests concerning the treatment of drag-and-drop URLs.")
(defvar dnd-tests-list-3 (append dnd-tests-list-2 '("http://example.com"))
"Sample data for tests concerning the treatment of drag-and-drop URLs.")
(defvar dnd-tests-list-4 (append dnd-tests-list-3 '("scheme1://foo.bar"
"scheme2://foo.bar"))
"Sample data for tests concerning the treatment of drag-and-drop URLs.")
(defun dnd-tests-local-file-function (urls _action)
"Signal an error if URLS doesn't match `dnd-tests-list-1'.
ACTION is ignored. Return the symbol `copy' otherwise."
(should (equal urls dnd-tests-list-1))
'copy)
(put 'dnd-tests-local-file-function 'dnd-multiple-handler t)
(defun dnd-tests-remote-file-function (urls _action)
"Signal an error if URLS doesn't match `dnd-tests-list-2'.
ACTION is ignored. Return the symbol `copy' otherwise."
(should (equal urls dnd-tests-list-2))
'copy)
(put 'dnd-tests-remote-file-function 'dnd-multiple-handler t)
(defun dnd-tests-http-scheme-function (url _action)
"Signal an error if URLS doesn't match `dnd-tests-list-3''s third element.
ACTION is ignored. Return the symbol `private' otherwise."
(should (equal url (car (last dnd-tests-list-3))))
'private)
(defun dnd-tests-browse-url-handler (url &rest _ignored)
"Verify URL is `dnd-tests-list-4''s fourth element."
(should (equal url (nth 3 dnd-tests-list-4))))
(put 'dnd-tests-browse-url-handler 'browse-url-browser-kind 'internal)
(ert-deftest dnd-tests-receive-multiple-urls ()
(let ((dnd-protocol-alist '(("^file:///" . dnd-tests-local-file-function)
("^file:" . error)
("^unrelated-scheme:" . error)))
(browse-url-handlers nil))
;; Check that the order of the alist is respected when the
;; precedences of two handlers are equal.
(should (equal (dnd-handle-multiple-urls (selected-window)
(copy-sequence
dnd-tests-list-1)
'copy)
'copy))
;; Check that sorting handlers by precedence functions correctly.
(setq dnd-protocol-alist '(("^file:///" . error)
("^file:" . dnd-tests-remote-file-function)
("^unrelated-scheme:" . error)))
(should (equal (dnd-handle-multiple-urls (selected-window)
(copy-sequence
dnd-tests-list-2)
'copy)
'copy))
;; Check that multiple handlers can be called at once, and actions
;; are properly "downgraded" to private when multiple handlers
;; return inconsistent values.
(setq dnd-protocol-alist '(("^file:" . dnd-tests-remote-file-function)
("^file:///" . error)
("^http://" . dnd-tests-http-scheme-function)))
(should (equal (dnd-handle-multiple-urls (selected-window)
(copy-sequence
dnd-tests-list-3)
'copy)
'private))
;; Now verify that the function's documented fallback behavior
;; functions correctly. Set browse-url-handlers to an association
;; list incorporating a test function, then guarantee that is
;; called.
(setq browse-url-handlers '(("^scheme1://" . dnd-tests-browse-url-handler)))
;; Furthermore, guarantee the fifth argument of the test data is
;; inserted, for no apposite handler exists.
(save-window-excursion
(set-window-buffer nil (get-buffer-create " *dnd-tests*"))
(set-buffer (get-buffer-create " *dnd-tests*"))
(erase-buffer)
(should (equal (dnd-handle-multiple-urls (selected-window)
(copy-sequence
dnd-tests-list-4)
'copy)
'private))
(should (equal (buffer-string) (nth 4 dnd-tests-list-4))))))
(provide 'dnd-tests)
;;; dnd-tests.el ends here