From: Po Lu Date: Wed, 16 Mar 2022 03:29:36 +0000 (+0800) Subject: Add support for dragging text from Emacs to other programs X-Git-Tag: emacs-29.0.90~1931^2~1110 X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=e53fba3fd4916029662e6619aba713d7dd7c7038;p=emacs.git Add support for dragging text from Emacs to other programs This still probably needs some more protection from malfunctioning clients which delete windows at random, but I don't know if that's a problem in practice. * doc/emacs/frames.texi (Drag and Drop): * doc/lispref/frames.texi (Drag and Drop): Document new features. * etc/NEWS: Announce new function `x-begin-drag' and new user option `mouse-drag-and-drop-region-cross-program'. * lisp/mouse.el (mouse-drag-and-drop-region-cross-program): New user option. (x-begin-drag): New variable declaration. (mouse-drag-and-drop-region): If the mouse moves out of an Emacs frame, begin a window system drag. * lisp/x-dnd.el (x-dnd-handle-xdnd): Remove left-over debugging code. * src/xfns.c (Fx_set_mouse_absolute_pixel_position): Fix indentation of opening paren. (Fx_begin_drag): New function. (syms_of_xfns): Define new subr. * src/xselect.c (x_timestamp_for_selection): New function. * src/xterm.c (X_DND_SUPPORTED_VERSION): New preprocessor declaration. (x_dnd_get_window_proto, x_dnd_send_enter, x_dnd_send_position) (x_dnd_send_leave, x_dnd_send_drop, x_set_dnd_targets) (x_dnd_begin_drag_and_drop): New functions. (handle_one_xevent): Handle drag-and-drop motion and button events when active. (x_free_frame_resources): If f is the DND source, stop drag-and-drop. (x_term_init): Intern new atoms. (syms_of_xterm): New symbol QXdndSelection. * src/xterm.h (struct x_display_info): New atoms Xatom_XdndAware, Xatom_XdndSelection, Xatom_XdndTypeList, Xatom_XdndActionCopy, Xatom_XdndActionMove, Xatom_XdndActionLink, Xatom_XdndActionAsk, Xatom_XdndActionPrivate, Xatom_XdndActionList, Xatom_XdndActionDescription, Xatom_XdndProxy, Xatom_XdndEnter, Xatom_XdndPosition, Xatom_XdndStatus, Xatom_XdndLeave, Xatom_XdndDrop, and Xatom_XdndFinished. --- diff --git a/doc/emacs/frames.texi b/doc/emacs/frames.texi index e3cfe5f8441..7489344cda9 100644 --- a/doc/emacs/frames.texi +++ b/doc/emacs/frames.texi @@ -1220,6 +1220,10 @@ cursor during dragging. To suppress such behavior, set the options @code{mouse-drag-and-drop-region-show-tooltip} and/or @code{mouse-drag-and-drop-region-show-cursor} to @code{nil}. +@vindex mouse-drag-and-drop-region-cross-program +To drag text from Emacs to other programs, set the option +@code{mouse-drag-and-drop-region-cross-program} to a non-@code{nil} +value. @node Menu Bars @section Menu Bars diff --git a/doc/lispref/frames.texi b/doc/lispref/frames.texi index bae8eb3c703..38897d6a0b3 100644 --- a/doc/lispref/frames.texi +++ b/doc/lispref/frames.texi @@ -4038,6 +4038,35 @@ there is no match there, Emacs looks for a match in still no match has been found, the text for the URL is inserted. If you want to alter Emacs behavior, you can customize these variables. +@cindex initiating drag-and-drop + On some window systems, Emacs also supports dragging contents from +itself to other frames. + +@defun x-begin-drag targets action &optional frame +This function begins a drag from @var{frame}, and returns when the +session ends, either because the drop was successful, or because the +drop was rejected. The drop occurs when all mouse buttons are +released on top of an X window other than @var{frame} (the @dfn{drop +target}). + +@var{targets} is a list of strings describing selection targets, much +like the @var{data-type} argument to @code{gui-get-selection}, that +the drop target can request from Emacs (@pxref{Window System +Selections}). + +@var{action} is a symbol describing the action recommended to the +target. It can either be @code{XdndActionCopy}, which means which +means to copy the contents of the selection @code{XdndSelection} to +the drop target, or @code{XdndActionMove}, which means the same as +@code{XdndActionCopy}, but also means the caller should delete +whatever was saved into that selection afterwards. + +If the drop was rejected or no drop target was found, this function +returns @code{nil}. Otherwise, it returns a symbol describing the +action the target chose to perform, which can differ from @var{action} +if that isn't supported by the drop target. +@end defun + @node Color Names @section Color Names diff --git a/etc/NEWS b/etc/NEWS index d6b5da3902e..f4d8756950b 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -169,6 +169,11 @@ methods instead. * Changes in Emacs 29.1 ++++ +** New user option 'mouse-drag-and-drop-region-cross-program'. +If non-nil, this option allows dragging text in the region from Emacs +to another program. + +++ ** New function 'command-query'. This function makes its argument command prompt the user for @@ -1203,6 +1208,11 @@ functions. * Lisp Changes in Emacs 29.1 ++++ +** New function 'x-begin-drag'. +This function initiates a drag-and-drop request with the contents of +the selection 'XdndSelection', and returns when a drop occurs. + --- ** New function 'ietf-drums-parse-date-string'. This function parses RFC5322 (and RFC822) date strings, and should be diff --git a/lisp/mouse.el b/lisp/mouse.el index 1e205283de2..3e2097e761f 100644 --- a/lisp/mouse.el +++ b/lisp/mouse.el @@ -2974,6 +2974,11 @@ in addition, temporarily highlight the original region with the :type 'boolean :version "26.1") +(defcustom mouse-drag-and-drop-region-cross-program nil + "If non-nil, allow dragging text to other programs." + :type 'boolean + :version "29.1") + (defface mouse-drag-and-drop-region '((t :inherit region)) "Face to highlight original text during dragging. This face is used by `mouse-drag-and-drop-region' to temporarily @@ -2984,6 +2989,7 @@ highlight the original region when (declare-function rectangle-dimensions "rect" (start end)) (declare-function rectangle-position-as-coordinates "rect" (position)) (declare-function rectangle-intersect-p "rect" (pos1 size1 pos2 size2)) +(declare-function x-begin-drag "xfns.c") (defun mouse-drag-and-drop-region (event) "Move text in the region to point where mouse is dragged to. @@ -3046,114 +3052,132 @@ is copied instead of being cut." states)))) (ignore-errors - (track-mouse - (setq track-mouse 'dropping) - ;; When event was "click" instead of "drag", skip loop. - (while (progn - (setq event (read-key)) ; read-event or read-key - (or (mouse-movement-p event) - ;; Handle `mouse-autoselect-window'. - (memq (car event) '(select-window switch-frame)))) - ;; Obtain the dragged text in region. When the loop was - ;; skipped, value-selection remains nil. - (unless value-selection - (setq value-selection (funcall region-extract-function nil)) - (when mouse-drag-and-drop-region-show-tooltip - (let ((text-size mouse-drag-and-drop-region-show-tooltip)) - (setq text-tooltip - (if (and (integerp text-size) - (> (length value-selection) text-size)) - (concat - (substring value-selection 0 (/ text-size 2)) - "\n...\n" - (substring value-selection (- (/ text-size 2)) -1)) - value-selection)))) - - ;; Check if selected text is read-only. - (setq text-from-read-only - (or text-from-read-only - (catch 'loop - (dolist (bound (region-bounds)) - (when (text-property-not-all - (car bound) (cdr bound) 'read-only nil) - (throw 'loop t))))))) - - (setq window-to-paste (posn-window (event-end event))) - (setq point-to-paste (posn-point (event-end event))) - ;; Set nil when target buffer is minibuffer. - (setq buffer-to-paste (let (buf) - (when (windowp window-to-paste) - (setq buf (window-buffer window-to-paste)) - (when (not (minibufferp buf)) - buf)))) - (setq cursor-in-text-area (and window-to-paste - point-to-paste - buffer-to-paste)) - - (when cursor-in-text-area - ;; Check if point under mouse is read-only. - (save-window-excursion - (select-window window-to-paste) - (setq point-to-paste-read-only - (or buffer-read-only - (get-text-property point-to-paste 'read-only)))) - - ;; Check if "drag but negligible". Operation "drag but - ;; negligible" is defined as drag-and-drop the text to - ;; the original region. When modifier is pressed, the - ;; text will be inserted to inside of the original - ;; region. - ;; - ;; If the region is rectangular, check if the newly inserted - ;; rectangular text would intersect the already selected - ;; region. If it would, then set "drag-but-negligible" to t. - ;; As a special case, allow dragging the region freely anywhere - ;; to the left, as this will never trigger its contents to be - ;; inserted into the overlays tracking it. - (setq drag-but-negligible - (and (eq (overlay-buffer (car mouse-drag-and-drop-overlays)) - buffer-to-paste) - (if region-noncontiguous - (let ((dimensions (rectangle-dimensions start end)) - (start-coordinates - (rectangle-position-as-coordinates start)) - (point-to-paste-coordinates - (rectangle-position-as-coordinates - point-to-paste))) - (and (rectangle-intersect-p - start-coordinates dimensions - point-to-paste-coordinates dimensions) - (not (< (car point-to-paste-coordinates) - (car start-coordinates))))) - (and (<= (overlay-start - (car mouse-drag-and-drop-overlays)) - point-to-paste) - (<= point-to-paste - (overlay-end - (car mouse-drag-and-drop-overlays)))))))) - - ;; Show a tooltip. - (if mouse-drag-and-drop-region-show-tooltip - (tooltip-show text-tooltip) - (tooltip-hide)) - - ;; Show cursor and highlight the original region. - (when mouse-drag-and-drop-region-show-cursor - ;; Modify cursor even when point is out of frame. - (setq cursor-type (cond - ((not cursor-in-text-area) - nil) - ((or point-to-paste-read-only - drag-but-negligible) - 'hollow) - (t - 'bar))) + (catch 'cross-program-drag + (track-mouse + (setq track-mouse 'dropping) + ;; When event was "click" instead of "drag", skip loop. + (while (progn + (setq event (read-key)) ; read-event or read-key + (or (mouse-movement-p event) + ;; Handle `mouse-autoselect-window'. + (memq (car event) '(select-window switch-frame)))) + ;; Obtain the dragged text in region. When the loop was + ;; skipped, value-selection remains nil. + (unless value-selection + (setq value-selection (funcall region-extract-function nil)) + (when mouse-drag-and-drop-region-show-tooltip + (let ((text-size mouse-drag-and-drop-region-show-tooltip)) + (setq text-tooltip + (if (and (integerp text-size) + (> (length value-selection) text-size)) + (concat + (substring value-selection 0 (/ text-size 2)) + "\n...\n" + (substring value-selection (- (/ text-size 2)) -1)) + value-selection)))) + + ;; Check if selected text is read-only. + (setq text-from-read-only + (or text-from-read-only + (catch 'loop + (dolist (bound (region-bounds)) + (when (text-property-not-all + (car bound) (cdr bound) 'read-only nil) + (throw 'loop t))))))) + + (when (and mouse-drag-and-drop-region-cross-program + (fboundp 'x-begin-drag) + (framep (posn-window (event-end event))) + (let ((location (posn-x-y (event-end event))) + (frame (posn-window (event-end event)))) + (or (< (car location) 0) + (< (cdr location) 0) + (> (car location) + (frame-pixel-width frame)) + (> (cdr location) + (frame-pixel-height frame))))) + (tooltip-hide) + (gui-set-selection 'XdndSelection value-selection) + (x-begin-drag '("UTF8_STRING" "STRING") + 'XdndActionMove (posn-window (event-end event))) + (throw 'cross-program-drag nil)) + + (setq window-to-paste (posn-window (event-end event))) + (setq point-to-paste (posn-point (event-end event))) + ;; Set nil when target buffer is minibuffer. + (setq buffer-to-paste (let (buf) + (when (windowp window-to-paste) + (setq buf (window-buffer window-to-paste)) + (when (not (minibufferp buf)) + buf)))) + (setq cursor-in-text-area (and window-to-paste + point-to-paste + buffer-to-paste)) + (when cursor-in-text-area - (dolist (overlay mouse-drag-and-drop-overlays) - (overlay-put overlay - 'face 'mouse-drag-and-drop-region)) - (deactivate-mark) ; Maintain region in other window. - (mouse-set-point event))))) + ;; Check if point under mouse is read-only. + (save-window-excursion + (select-window window-to-paste) + (setq point-to-paste-read-only + (or buffer-read-only + (get-text-property point-to-paste 'read-only)))) + + ;; Check if "drag but negligible". Operation "drag but + ;; negligible" is defined as drag-and-drop the text to + ;; the original region. When modifier is pressed, the + ;; text will be inserted to inside of the original + ;; region. + ;; + ;; If the region is rectangular, check if the newly inserted + ;; rectangular text would intersect the already selected + ;; region. If it would, then set "drag-but-negligible" to t. + ;; As a special case, allow dragging the region freely anywhere + ;; to the left, as this will never trigger its contents to be + ;; inserted into the overlays tracking it. + (setq drag-but-negligible + (and (eq (overlay-buffer (car mouse-drag-and-drop-overlays)) + buffer-to-paste) + (if region-noncontiguous + (let ((dimensions (rectangle-dimensions start end)) + (start-coordinates + (rectangle-position-as-coordinates start)) + (point-to-paste-coordinates + (rectangle-position-as-coordinates + point-to-paste))) + (and (rectangle-intersect-p + start-coordinates dimensions + point-to-paste-coordinates dimensions) + (not (< (car point-to-paste-coordinates) + (car start-coordinates))))) + (and (<= (overlay-start + (car mouse-drag-and-drop-overlays)) + point-to-paste) + (<= point-to-paste + (overlay-end + (car mouse-drag-and-drop-overlays)))))))) + + ;; Show a tooltip. + (if mouse-drag-and-drop-region-show-tooltip + (tooltip-show text-tooltip) + (tooltip-hide)) + + ;; Show cursor and highlight the original region. + (when mouse-drag-and-drop-region-show-cursor + ;; Modify cursor even when point is out of frame. + (setq cursor-type (cond + ((not cursor-in-text-area) + nil) + ((or point-to-paste-read-only + drag-but-negligible) + 'hollow) + (t + 'bar))) + (when cursor-in-text-area + (dolist (overlay mouse-drag-and-drop-overlays) + (overlay-put overlay + 'face 'mouse-drag-and-drop-region)) + (deactivate-mark) ; Maintain region in other window. + (mouse-set-point event)))))) ;; Hide a tooltip. (when mouse-drag-and-drop-region-show-tooltip (tooltip-hide)) diff --git a/lisp/x-dnd.el b/lisp/x-dnd.el index 559679131bd..0529d223dbe 100644 --- a/lisp/x-dnd.el +++ b/lisp/x-dnd.el @@ -446,7 +446,6 @@ FORMAT is 32 (not used). MESSAGE is the data part of an XClientMessageEvent." (version (x-dnd-version-from-flags flags)) (more-than-3 (x-dnd-more-than-3-from-flags flags)) (dnd-source (aref data 0))) - (message "%s %s" version more-than-3) (if version ;; If flags is bad, version will be nil. (x-dnd-save-state window nil nil diff --git a/src/xfns.c b/src/xfns.c index c8aefec8d71..3e184571a06 100644 --- a/src/xfns.c +++ b/src/xfns.c @@ -6547,7 +6547,7 @@ DEFUN ("x-set-mouse-absolute-pixel-position", Fx_set_mouse_absolute_pixel_positi The coordinates X and Y are interpreted in pixels relative to a position \(0, 0) of the selected frame's display. */) (Lisp_Object x, Lisp_Object y) - { +{ struct frame *f = SELECTED_FRAME (); if (FRAME_INITIAL_P (f) || !FRAME_X_P (f)) @@ -6582,6 +6582,85 @@ The coordinates X and Y are interpreted in pixels relative to a position return Qnil; } +DEFUN ("x-begin-drag", Fx_begin_drag, Sx_begin_drag, 1, 3, 0, + doc: /* Begin dragging contents on FRAME, with targets TARGETS. +TARGETS is a list of strings, which defines the X selection targets +that will be available to the drop target. Dragging starts when the +mouse is pressed on FRAME, and the contents of the selection +`XdndSelection' will be sent to the X window underneath the mouse +pointer (the drop target) when the mouse button is released. ACTION +is a symbol which tells the target what to do, and can be one of the +following: + + - `XdndActionCopy', which means to copy the contents from the drag + source (FRAME) to the drop target. + + - `XdndActionMove', which means to first take the contents of + `XdndSelection', and to delete whatever was saved into that + selection afterwards. + +There are also some other valid values of ACTION that depend on +details of both the drop target's implementation details and that of +Emacs. For that reason, they are not mentioned here. Consult +"Drag-and-Drop Protocol for the X Window System" for more details: +https://freedesktop.org/wiki/Specifications/XDND/. + +If ACTION is not specified or nil, `XdndActionCopy' is used instead. + +Block until the mouse buttons are released, then return the action +chosen by the target, or `nil' if the drop was not accepted by the +drop target. */) + (Lisp_Object targets, Lisp_Object action, Lisp_Object frame) +{ + struct frame *f = decode_window_system_frame (frame); + int ntargets = 0; + char *target_names[2048]; + Atom *target_atoms; + Lisp_Object lval; + Atom xaction; + + CHECK_LIST (targets); + + for (; CONSP (targets); targets = XCDR (targets)) + { + CHECK_STRING (XCAR (targets)); + + if (ntargets < 2048) + { + target_names[ntargets] = SSDATA (XCAR (targets)); + ntargets++; + } + else + error ("Too many targets"); + } + + if (NILP (action) || EQ (action, QXdndActionCopy)) + xaction = FRAME_DISPLAY_INFO (f)->Xatom_XdndActionCopy; + else if (EQ (action, QXdndActionMove)) + xaction = FRAME_DISPLAY_INFO (f)->Xatom_XdndActionMove; + else if (EQ (action, QXdndActionLink)) + xaction = FRAME_DISPLAY_INFO (f)->Xatom_XdndActionLink; + else if (EQ (action, QXdndActionAsk)) + xaction = FRAME_DISPLAY_INFO (f)->Xatom_XdndActionAsk; + else if (EQ (action, QXdndActionPrivate)) + xaction = FRAME_DISPLAY_INFO (f)->Xatom_XdndActionPrivate; + else + signal_error ("Invalid drag-and-drop action", action); + + target_atoms = xmalloc (ntargets * sizeof *target_atoms); + + block_input (); + XInternAtoms (FRAME_X_DISPLAY (f), target_names, + ntargets, False, target_atoms); + unblock_input (); + + x_set_dnd_targets (target_atoms, ntargets); + lval = x_dnd_begin_drag_and_drop (f, FRAME_DISPLAY_INFO (f)->last_user_time, + xaction); + + return lval; +} + /************************************************************************ X Displays ************************************************************************/ @@ -9150,6 +9229,12 @@ syms_of_xfns (void) DEFSYM (Qreverse_landscape, "reverse-landscape"); #endif + DEFSYM (QXdndActionCopy, "XdndActionCopy"); + DEFSYM (QXdndActionMove, "XdndActionMove"); + DEFSYM (QXdndActionLink, "XdndActionLink"); + DEFSYM (QXdndActionAsk, "XdndActionAsk"); + DEFSYM (QXdndActionPrivate, "XdndActionPrivate"); + Fput (Qundefined_color, Qerror_conditions, pure_list (Qundefined_color, Qerror)); Fput (Qundefined_color, Qerror_message, @@ -9423,6 +9508,7 @@ eliminated in future versions of Emacs. */); defsubr (&Sx_show_tip); defsubr (&Sx_hide_tip); defsubr (&Sx_double_buffered_p); + defsubr (&Sx_begin_drag); tip_timer = Qnil; staticpro (&tip_timer); tip_frame = Qnil; diff --git a/src/xselect.c b/src/xselect.c index a88c15aa95b..cdc70d3e247 100644 --- a/src/xselect.c +++ b/src/xselect.c @@ -2647,6 +2647,25 @@ x_send_client_event (Lisp_Object display, Lisp_Object dest, Lisp_Object from, +/* Return the timestamp where ownership of SELECTION was asserted, or + nil if no local selection is present. */ + +Lisp_Object +x_timestamp_for_selection (struct x_display_info *dpyinfo, + Lisp_Object selection) +{ + Lisp_Object value, local_value; + + local_value = LOCAL_SELECTION (selection, dpyinfo); + + if (NILP (local_value)) + return Qnil; + + value = XCAR (XCDR (XCDR (local_value))); + + return value; +} + static void syms_of_xselect_for_pdumper (void); void diff --git a/src/xterm.c b/src/xterm.c index fc90e370430..8a4344f2a4f 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -770,6 +770,338 @@ static void x_update_opaque_region (struct frame *, XEvent *); static void x_scroll_bar_end_update (struct x_display_info *, struct scroll_bar *); #endif +static bool x_dnd_in_progress; +static Window x_dnd_last_seen_window; +static int x_dnd_last_protocol_version; +static Time x_dnd_selection_timestamp; + +static Window x_dnd_mouse_rect_target; +static XRectangle x_dnd_mouse_rect; +static Atom x_dnd_action; +static Atom x_dnd_wanted_action; + +static Atom *x_dnd_targets = NULL; +static int x_dnd_n_targets; +static struct frame *x_dnd_frame; + +#define X_DND_SUPPORTED_VERSION 5 + +static Window +x_dnd_get_target_window (struct x_display_info *dpyinfo, + int root_x, int root_y) +{ + Window child_return, child, dummy, proxy; + int dest_x_return, dest_y_return; + int rc; + int actual_format; + unsigned long actual_size, bytes_remaining; + unsigned char *tmp_data; + XWindowAttributes attrs; + Atom actual_type; + + child_return = dpyinfo->root_window; + dest_x_return = root_x; + dest_y_return = root_y; + + /* Not strictly necessary, but satisfies GCC. */ + child = dpyinfo->root_window; + + while (child_return != None) + { + child = child_return; + + x_catch_errors (dpyinfo->display); + rc = XTranslateCoordinates (dpyinfo->display, + child_return, child_return, + dest_x_return, dest_y_return, + &dest_x_return, &dest_y_return, + &child_return); + + if (x_had_errors_p (dpyinfo->display) || !rc) + { + x_uncatch_errors_after_check (); + break; + } + + if (child_return) + { + rc = XTranslateCoordinates (dpyinfo->display, + child, child_return, + dest_x_return, dest_y_return, + &dest_x_return, &dest_y_return, + &dummy); + + if (x_had_errors_p (dpyinfo->display) || !rc) + { + x_uncatch_errors_after_check (); + return None; + } + } + + x_uncatch_errors_after_check (); + } + + if (child != None) + { + x_catch_errors (dpyinfo->display); + rc = XGetWindowProperty (dpyinfo->display, child, + dpyinfo->Xatom_XdndProxy, + 0, 1, False, XA_WINDOW, + &actual_type, &actual_format, + &actual_size, &bytes_remaining, + &tmp_data); + + if (!x_had_errors_p (dpyinfo->display) + && rc == Success + && actual_type == XA_WINDOW + && actual_format == 32 + && actual_size == 1) + { + proxy = *(Window *) tmp_data; + XFree (tmp_data); + + /* Verify the proxy window exists. */ + XGetWindowAttributes (dpyinfo->display, proxy, &attrs); + + if (!x_had_errors_p (dpyinfo->display)) + child = proxy; + } + + x_uncatch_errors_after_check (); + } + + return child; +} + +static int +x_dnd_get_window_proto (struct x_display_info *dpyinfo, Window wdesc) +{ + Atom actual, value; + unsigned char *tmp_data; + int rc, format; + unsigned long n, left; + bool had_errors; + + if (wdesc == None || wdesc == FRAME_X_WINDOW (x_dnd_frame)) + return -1; + + x_catch_errors (dpyinfo->display); + rc = XGetWindowProperty (dpyinfo->display, wdesc, dpyinfo->Xatom_XdndAware, + 0, 1, False, XA_ATOM, &actual, &format, &n, &left, + &tmp_data); + had_errors = x_had_errors_p (dpyinfo->display); + x_uncatch_errors_after_check (); + + if (had_errors || rc != Success || actual != XA_ATOM || format != 32 || n < 1) + return -1; + + value = (int) *(Atom *) tmp_data; + XFree (tmp_data); + + return (int) value; +} + +static void +x_dnd_send_enter (struct frame *f, Window target, int supported) +{ + struct x_display_info *dpyinfo = FRAME_DISPLAY_INFO (f); + int i; + XEvent msg; + + msg.xclient.type = ClientMessage; + msg.xclient.message_type = dpyinfo->Xatom_XdndEnter; + msg.xclient.format = 32; + msg.xclient.window = target; + msg.xclient.data.l[0] = FRAME_X_WINDOW (f); + msg.xclient.data.l[1] = (((unsigned int) min (X_DND_SUPPORTED_VERSION, + supported) << 24) + | (x_dnd_n_targets > 3 ? 1 : 0)); + msg.xclient.data.l[2] = 0; + msg.xclient.data.l[3] = 0; + msg.xclient.data.l[4] = 0; + + for (i = 0; i < min (3, x_dnd_n_targets); ++i) + msg.xclient.data.l[i + 2] = x_dnd_targets[i]; + + if (x_dnd_n_targets > 3) + XChangeProperty (FRAME_X_DISPLAY (f), FRAME_X_WINDOW (f), + dpyinfo->Xatom_XdndTypeList, XA_ATOM, 32, + PropModeReplace, (unsigned char *) x_dnd_targets, + x_dnd_n_targets); + + XSendEvent (FRAME_X_DISPLAY (f), target, False, 0, &msg); +} + +static void +x_dnd_send_position (struct frame *f, Window target, int supported, + unsigned short root_x, unsigned short root_y, + Time timestamp, Atom action) +{ + struct x_display_info *dpyinfo = FRAME_DISPLAY_INFO (f); + XEvent msg; + + if (target == x_dnd_mouse_rect_target + && x_dnd_mouse_rect.width + && x_dnd_mouse_rect.height) + { + if (root_x >= x_dnd_mouse_rect.x + && root_x < (x_dnd_mouse_rect.x + + x_dnd_mouse_rect.width) + && root_y >= x_dnd_mouse_rect.y + && root_y < (x_dnd_mouse_rect.y + + x_dnd_mouse_rect.height)) + return; + } + + msg.xclient.type = ClientMessage; + msg.xclient.message_type = dpyinfo->Xatom_XdndPosition; + msg.xclient.format = 32; + msg.xclient.window = target; + msg.xclient.data.l[0] = FRAME_X_WINDOW (f); + msg.xclient.data.l[1] = 0; + msg.xclient.data.l[2] = (root_x << 16) | root_y; + msg.xclient.data.l[3] = 0; + msg.xclient.data.l[4] = 0; + + if (supported >= 3) + msg.xclient.data.l[3] = timestamp; + + if (supported >= 4) + msg.xclient.data.l[4] = action; + + XSendEvent (FRAME_X_DISPLAY (f), target, False, 0, &msg); +} + +static void +x_dnd_send_leave (struct frame *f, Window target) +{ + struct x_display_info *dpyinfo = FRAME_DISPLAY_INFO (f); + XEvent msg; + + msg.xclient.type = ClientMessage; + msg.xclient.message_type = dpyinfo->Xatom_XdndLeave; + msg.xclient.format = 32; + msg.xclient.window = target; + msg.xclient.data.l[0] = FRAME_X_WINDOW (f); + msg.xclient.data.l[1] = 0; + msg.xclient.data.l[2] = 0; + msg.xclient.data.l[3] = 0; + msg.xclient.data.l[4] = 0; + + XSendEvent (FRAME_X_DISPLAY (f), target, False, 0, &msg); +} + +static void +x_dnd_send_drop (struct frame *f, Window target, Time timestamp, + int supported) +{ + struct x_display_info *dpyinfo = FRAME_DISPLAY_INFO (f); + XEvent msg; + + msg.xclient.type = ClientMessage; + msg.xclient.message_type = dpyinfo->Xatom_XdndDrop; + msg.xclient.format = 32; + msg.xclient.window = target; + msg.xclient.data.l[0] = FRAME_X_WINDOW (f); + msg.xclient.data.l[1] = 0; + msg.xclient.data.l[2] = 0; + msg.xclient.data.l[3] = 0; + msg.xclient.data.l[4] = 0; + + if (supported >= 1) + msg.xclient.data.l[2] = timestamp; + + XSendEvent (FRAME_X_DISPLAY (f), target, False, 0, &msg); +} + +void +x_set_dnd_targets (Atom *targets, int ntargets) +{ + if (x_dnd_targets) + xfree (x_dnd_targets); + + x_dnd_targets = targets; + x_dnd_n_targets = ntargets; +} + +Lisp_Object +x_dnd_begin_drag_and_drop (struct frame *f, Time time, Atom xaction) +{ + XEvent next_event; + struct input_event hold_quit; + int finish; + char *atom_name; + Lisp_Object action, ltimestamp; + + if (x_dnd_in_progress) + error ("A drag-and-drop session is already in progress"); + + ltimestamp = x_timestamp_for_selection (FRAME_DISPLAY_INFO (f), + QXdndSelection); + + if (NILP (ltimestamp)) + error ("No local value for XdndSelection"); + + if (BIGNUMP (ltimestamp)) + x_dnd_selection_timestamp = bignum_to_intmax (ltimestamp); + else + x_dnd_selection_timestamp = XFIXNUM (ltimestamp); + + x_dnd_in_progress = true; + x_dnd_frame = f; + x_dnd_last_seen_window = FRAME_X_WINDOW (f); + x_dnd_last_protocol_version = -1; + x_dnd_mouse_rect_target = None; + x_dnd_action = None; + x_dnd_wanted_action = xaction; + + while (x_dnd_in_progress) + { + hold_quit.kind = NO_EVENT; + + block_input (); + XNextEvent (FRAME_X_DISPLAY (f), &next_event); + + handle_one_xevent (FRAME_DISPLAY_INFO (f), + &next_event, &finish, &hold_quit); + unblock_input (); + + if (hold_quit.kind != NO_EVENT) + { + if (x_dnd_in_progress) + { + block_input (); + if (x_dnd_last_seen_window != None + && x_dnd_last_protocol_version != -1) + x_dnd_send_leave (f, x_dnd_last_seen_window); + unblock_input (); + + x_dnd_in_progress = false; + x_dnd_frame = NULL; + } + + FRAME_DISPLAY_INFO (f)->grabbed = 0; + quit (); + } + } + + FRAME_DISPLAY_INFO (f)->grabbed = 0; + + if (x_dnd_wanted_action != None) + { + block_input (); + atom_name = XGetAtomName (FRAME_X_DISPLAY (f), + x_dnd_wanted_action); + action = intern (atom_name); + XFree (atom_name); + unblock_input (); + + return action; + } + + return Qnil; +} + /* Flush display of frame F. */ static void @@ -10084,6 +10416,42 @@ handle_one_xevent (struct x_display_info *dpyinfo, { case ClientMessage: { + if (x_dnd_in_progress + && FRAME_DISPLAY_INFO (x_dnd_frame) == dpyinfo + && event->xclient.message_type == dpyinfo->Xatom_XdndStatus) + { + Window target; + + target = event->xclient.data.l[0]; + + if (x_dnd_last_protocol_version != -1 + && target == x_dnd_last_seen_window + && event->xclient.data.l[1] & 2) + { + x_dnd_mouse_rect_target = target; + x_dnd_mouse_rect.x = (event->xclient.data.l[2] & 0xffff0000) >> 16; + x_dnd_mouse_rect.y = (event->xclient.data.l[2] & 0xffff); + x_dnd_mouse_rect.width = (event->xclient.data.l[3] & 0xffff0000) >> 16; + x_dnd_mouse_rect.height = (event->xclient.data.l[3] & 0xffff); + } + else + x_dnd_mouse_rect_target = None; + + if (x_dnd_last_protocol_version != -1 + && target == x_dnd_last_seen_window) + { + if (event->xclient.data.l[1] & 1) + { + if (x_dnd_last_protocol_version >= 2) + x_dnd_wanted_action = event->xclient.data.l[4]; + else + x_dnd_wanted_action = dpyinfo->Xatom_XdndActionCopy; + } + else + x_dnd_wanted_action = None; + } + } + if (event->xclient.message_type == dpyinfo->Xatom_wm_protocols && event->xclient.format == 32) { @@ -11222,6 +11590,43 @@ handle_one_xevent (struct x_display_info *dpyinfo, clear_mouse_face (hlinfo); } + if (x_dnd_in_progress + && dpyinfo == FRAME_DISPLAY_INFO (x_dnd_frame)) + { + Window target; + + target = x_dnd_get_target_window (dpyinfo, + event->xmotion.x_root, + event->xmotion.y_root); + + if (target != x_dnd_last_seen_window) + { + if (x_dnd_last_seen_window != None + && x_dnd_last_protocol_version != -1 + && x_dnd_last_seen_window != FRAME_X_WINDOW (x_dnd_frame)) + x_dnd_send_leave (x_dnd_frame, x_dnd_last_seen_window); + + x_dnd_wanted_action = None; + x_dnd_last_seen_window = target; + x_dnd_last_protocol_version + = x_dnd_get_window_proto (dpyinfo, target); + + if (target != None && x_dnd_last_protocol_version != -1) + x_dnd_send_enter (x_dnd_frame, target, + x_dnd_last_protocol_version); + } + + if (x_dnd_last_protocol_version != -1 && target != None) + x_dnd_send_position (x_dnd_frame, target, + x_dnd_last_protocol_version, + event->xmotion.x_root, + event->xmotion.y_root, + x_dnd_selection_timestamp, + dpyinfo->Xatom_XdndActionCopy); + + goto OTHER; + } + f = mouse_or_wdesc_frame (dpyinfo, event->xmotion.window); #ifdef USE_GTK @@ -11573,6 +11978,38 @@ handle_one_xevent (struct x_display_info *dpyinfo, Lisp_Object tab_bar_arg = Qnil; bool tab_bar_p = false; bool tool_bar_p = false; + bool dnd_grab = false; + + for (int i = 1; i < 8; ++i) + { + if (i != event->xbutton.button + && event->xbutton.state & (Button1Mask << (i - 1))) + dnd_grab = true; + } + + if (x_dnd_in_progress + && dpyinfo == FRAME_DISPLAY_INFO (x_dnd_frame) + && !dnd_grab + && event->xbutton.type == ButtonRelease) + { + x_dnd_in_progress = false; + + if (x_dnd_last_seen_window != None + && x_dnd_last_protocol_version != -1) + x_dnd_send_drop (x_dnd_frame, x_dnd_last_seen_window, + x_dnd_selection_timestamp, + x_dnd_last_protocol_version); + + x_dnd_last_protocol_version = -1; + x_dnd_last_seen_window = None; + x_dnd_frame = NULL; + x_set_dnd_targets (NULL, 0); + + goto OTHER; + } + + if (x_dnd_in_progress) + goto OTHER; memset (&compose_status, 0, sizeof (compose_status)); dpyinfo->last_mouse_glyph_frame = NULL; @@ -12372,6 +12809,41 @@ handle_one_xevent (struct x_display_info *dpyinfo, clear_mouse_face (hlinfo); } + if (x_dnd_in_progress + && dpyinfo == FRAME_DISPLAY_INFO (x_dnd_frame)) + { + Window target; + + target = x_dnd_get_target_window (dpyinfo, + xev->root_x, + xev->root_y); + + if (target != x_dnd_last_seen_window) + { + if (x_dnd_last_seen_window != None + && x_dnd_last_protocol_version != -1 + && x_dnd_last_seen_window != FRAME_X_WINDOW (x_dnd_frame)) + x_dnd_send_leave (x_dnd_frame, x_dnd_last_seen_window); + + x_dnd_last_seen_window = target; + x_dnd_last_protocol_version + = x_dnd_get_window_proto (dpyinfo, target); + + if (target != None && x_dnd_last_protocol_version != -1) + x_dnd_send_enter (x_dnd_frame, target, + x_dnd_last_protocol_version); + } + + if (x_dnd_last_protocol_version != -1 && target != None) + x_dnd_send_position (x_dnd_frame, target, + x_dnd_last_protocol_version, + xev->root_x, xev->root_y, + x_dnd_selection_timestamp, + dpyinfo->Xatom_XdndActionCopy); + + goto XI_OTHER; + } + f = mouse_or_wdesc_frame (dpyinfo, xev->event); #ifdef USE_GTK @@ -12467,6 +12939,37 @@ handle_one_xevent (struct x_display_info *dpyinfo, #endif /* A fake XButtonEvent for x_construct_mouse_click. */ XButtonEvent bv; + bool dnd_grab = false; + + for (int i = 0; i < xev->buttons.mask_len * 8; ++i) + { + if (i != xev->detail && XIMaskIsSet (xev->buttons.mask, i)) + dnd_grab = true; + } + + if (x_dnd_in_progress + && dpyinfo == FRAME_DISPLAY_INFO (x_dnd_frame) + && !dnd_grab + && xev->evtype == XI_ButtonRelease) + { + x_dnd_in_progress = false; + + if (x_dnd_last_seen_window != None + && x_dnd_last_protocol_version != -1) + x_dnd_send_drop (x_dnd_frame, x_dnd_last_seen_window, + x_dnd_selection_timestamp, + x_dnd_last_protocol_version); + + x_dnd_last_protocol_version = -1; + x_dnd_last_seen_window = None; + x_dnd_frame = NULL; + x_set_dnd_targets (NULL, 0); + + goto XI_OTHER; + } + + if (x_dnd_in_progress) + goto XI_OTHER; #ifdef USE_MOTIF #ifdef USE_TOOLKIT_SCROLL_BARS @@ -16554,6 +17057,16 @@ x_free_frame_resources (struct frame *f) struct scroll_bar *b; #endif + if (x_dnd_in_progress && f == x_dnd_frame) + { + if (x_dnd_last_seen_window != None + && x_dnd_last_protocol_version != -1) + x_dnd_send_leave (f, x_dnd_last_seen_window); + + x_dnd_in_progress = false; + x_dnd_frame = NULL; + } + block_input (); /* If a display connection is dead, don't try sending more @@ -18014,6 +18527,24 @@ x_term_init (Lisp_Object display_name, char *xrm_option, char *resource_name) ATOM_REFS_INIT ("ShiftLock", Xatom_ShiftLock) ATOM_REFS_INIT ("Alt", Xatom_Alt) #endif + /* DND source. */ + ATOM_REFS_INIT ("XdndAware", Xatom_XdndAware) + ATOM_REFS_INIT ("XdndSelection", Xatom_XdndSelection) + ATOM_REFS_INIT ("XdndTypeList", Xatom_XdndTypeList) + ATOM_REFS_INIT ("XdndActionCopy", Xatom_XdndActionCopy) + ATOM_REFS_INIT ("XdndActionMove", Xatom_XdndActionMove) + ATOM_REFS_INIT ("XdndActionLink", Xatom_XdndActionLink) + ATOM_REFS_INIT ("XdndActionAsk", Xatom_XdndActionAsk) + ATOM_REFS_INIT ("XdndActionPrivate", Xatom_XdndActionPrivate) + ATOM_REFS_INIT ("XdndActionList", Xatom_XdndActionList) + ATOM_REFS_INIT ("XdndActionDescription", Xatom_XdndActionDescription) + ATOM_REFS_INIT ("XdndProxy", Xatom_XdndProxy) + ATOM_REFS_INIT ("XdndEnter", Xatom_XdndEnter) + ATOM_REFS_INIT ("XdndPosition", Xatom_XdndPosition) + ATOM_REFS_INIT ("XdndStatus", Xatom_XdndStatus) + ATOM_REFS_INIT ("XdndLeave", Xatom_XdndLeave) + ATOM_REFS_INIT ("XdndDrop", Xatom_XdndDrop) + ATOM_REFS_INIT ("XdndFinished", Xatom_XdndFinished) }; int i; @@ -18689,6 +19220,7 @@ With MS Windows, Haiku windowing or Nextstep, the value is t. */); Fput (Qmeta, Qmodifier_value, make_fixnum (meta_modifier)); DEFSYM (Qsuper, "super"); Fput (Qsuper, Qmodifier_value, make_fixnum (super_modifier)); + DEFSYM (QXdndSelection, "XdndSelection"); DEFVAR_LISP ("x-ctrl-keysym", Vx_ctrl_keysym, doc: /* Which keys Emacs uses for the ctrl modifier. diff --git a/src/xterm.h b/src/xterm.h index 3638f322e51..225aaf4cad5 100644 --- a/src/xterm.h +++ b/src/xterm.h @@ -561,6 +561,14 @@ struct x_display_info /* SM */ Atom Xatom_SM_CLIENT_ID; + /* DND source. */ + Atom Xatom_XdndAware, Xatom_XdndSelection, Xatom_XdndTypeList, + Xatom_XdndActionCopy, Xatom_XdndActionMove, Xatom_XdndActionLink, + Xatom_XdndActionAsk, Xatom_XdndActionPrivate, Xatom_XdndActionList, + Xatom_XdndActionDescription, Xatom_XdndProxy, Xatom_XdndEnter, + Xatom_XdndPosition, Xatom_XdndStatus, Xatom_XdndLeave, Xatom_XdndDrop, + Xatom_XdndFinished; + #ifdef HAVE_XKB /* Virtual modifiers */ Atom Xatom_Meta, Xatom_Super, Xatom_Hyper, Xatom_ShiftLock, Xatom_Alt; @@ -1359,6 +1367,9 @@ extern void x_scroll_bar_configure (GdkEvent *); extern void x_display_set_last_user_time (struct x_display_info *, Time); +extern Lisp_Object x_dnd_begin_drag_and_drop (struct frame *, Time, Atom); +extern void x_set_dnd_targets (Atom *, int); + INLINE int x_display_pixel_height (struct x_display_info *dpyinfo) { @@ -1453,6 +1464,9 @@ extern Lisp_Object x_property_data_to_lisp (struct frame *, extern void x_clipboard_manager_save_frame (Lisp_Object); extern void x_clipboard_manager_save_all (void); +extern Lisp_Object x_timestamp_for_selection (struct x_display_info *, + Lisp_Object); + #ifdef USE_GTK extern bool xg_set_icon (struct frame *, Lisp_Object); extern bool xg_set_icon_from_xpm_data (struct frame *, const char **);