Subject: bug#3468: drag and drop text

From: Cecilio Pardo <[email-masked]>
Date: Sat, 28 Sep 2024 23:52:45 +0200

This patch implements drag-and-drop for w32 for files and text, using C,
not C++.
This should work from Windows 95, but I can't test it.  Because of this,
the prior implementation with WM_DROPFILES has been removed with '#if
0', and can probably be completely removed.

I tested with mingw-w64 and mingw on Windows 11.

Text is inserted at point position, not at drop position.  I don't know
how this works on other platforms.

From 1712ab3ba109b2beafa418f0b12d09c836590b2c Mon Sep 17 00:00:00 2001
From: Cecilio Pardo <[email-masked]>
Date: Fri, 27 Sep 2024 23:58:02 +0200
Subject: [PATCH] Implement drag-n-drop for w32 with support for files and text

Implement dnd with IDropTarget

* lisp/term/w32-win.el (w32-drag-n-drop): changed to handle files or strings
* src/w32fns.c
(process_dropfiles): new, convert DROPFILES struct to array of strings
(w32_createwindow): assign an IDropTarget to each new frame
(w32_name_of_message): added new messages
(w32_msg_pump): Changed CoInitialize to OleInitialize, needed by the
drag-n-drop functions
(w32_wnd_proc): new struct w32_drop_target, and w32_drop_target_*
functions to implement the IDropTarget interface
* src/w32term.c (w32_read_socket): handle WM_EMACS_DROPFILES,
WM_EMACS_DROPSTRING, skip WM_EMACS_DROPFILES
* src/w32term.h (): add WM_EMACS_DROPFILES, WM_EMACS_DROPSTRING
---
 lisp/term/w32-win.el |  19 ++--
 src/w32fns.c         | 202 ++++++++++++++++++++++++++++++++++++++++++-
 src/w32term.c        |  46 ++++++++++
 src/w32term.h        |   4 +-
 4 files changed, 260 insertions(+), 11 deletions(-)

diff --git a/lisp/term/w32-win.el b/lisp/term/w32-win.el
index b57b3dd3bef..ac85ec8d945 100644
--- a/lisp/term/w32-win.el
+++ b/lisp/term/w32-win.el
@@ -131,8 +131,11 @@ w32-dropped-file-to-url
    file-name))
 
 (defun w32-drag-n-drop (event &optional new-frame)
-  "Edit the files listed in the drag-n-drop EVENT.
-Switch to a buffer editing the last file dropped."
+  "If the drag-n-drop EVENT is for a file or files, edit those
+files. Switch to a buffer editing the last file dropped.
+
+If the EVENT is for text, insert the text at point on the buffer shown
+in the target window, or add to kill ring if that buffer is read-only."
   (interactive "e")
   (save-excursion
     ;; Make sure the drop target has positive co-ords
@@ -140,6 +143,7 @@ w32-drag-n-drop
     ;; won't work.  <[email-masked]>
     (let* ((window (posn-window (event-start event)))
 	   (coords (posn-x-y (event-start event)))
+           (arg (car (cdr (cdr event))))
 	   (x (car coords))
 	   (y (cdr coords)))
       (if (and (> x 0) (> y 0))
@@ -150,11 +154,12 @@ w32-drag-n-drop
       (raise-frame)
       (setq window (selected-window))
 
-      (dnd-handle-multiple-urls
-       window
-       (mapcar #'w32-dropped-file-to-url
-               (car (cdr (cdr event))))
-       'private))))
+      (if (stringp arg)
+          (dnd-insert-text window 'copy arg)
+        (dnd-handle-multiple-urls
+         window
+         (mapcar #'w32-dropped-file-to-url arg)
+         'private)))))
 
 (defun w32-drag-n-drop-other-frame (event)
   "Edit the files listed in the drag-n-drop EVENT, in other frames.
diff --git a/src/w32fns.c b/src/w32fns.c
index 0a3f5c38a58..fdba3524846 100644
--- a/src/w32fns.c
+++ b/src/w32fns.c
@@ -34,6 +34,12 @@ #define _WIN32_WINNT 0x0600
 
 #include <c-ctype.h>
 
+#define COBJMACROS /* Ask for C definitions for COM.  */
+#include <shlobj.h>
+#include <oleidl.h>
+#include <objidl.h>
+#include <ole2.h>
+
 #include "lisp.h"
 #include "w32term.h"
 #include "frame.h"
@@ -359,6 +365,10 @@ #define WS_EX_NOACTIVATE 0x08000000L
 
 static struct w32_display_info *w32_display_info_for_name (Lisp_Object);
 
+static void my_post_msg (W32Msg*, HWND, UINT, WPARAM, LPARAM);
+static unsigned int w32_get_modifiers (void);
+
+
 /* Let the user specify a display with a frame.
    nil stands for the selected frame--or, if that is not a w32 frame,
    the first display on the list.  */
@@ -2464,6 +2474,168 @@ w32_createhscrollbar (struct frame *f, struct scroll_bar * bar)
   return hwnd;
 }
 
+/* From the DROPFILES struct, extract the list of filenames.  Returns a
+   NULL terminated malloc array of malloc strings that should be freed
+   by the caller.  */
+static char **
+process_dropfiles (DROPFILES *files)
+{
+  char *start_of_files = (char*)files + files->pFiles;
+  int count = 0;
+  char filename[MAX_PATH];
+  char **filenames;
+
+  if (files->fWide)
+    {
+      WCHAR *p = (WCHAR*)start_of_files;
+      for ( ; *p; count ++, p += wcslen (p) + 1)
+	;
+      filenames = malloc ((count+1) * sizeof (char*));
+      filenames [count] = NULL;
+      p = (WCHAR*)start_of_files;
+      for ( int i = 0; *p; p += wcslen (p) + 1, i++)
+	{
+	  filename_from_utf16 (p, filename);
+	  filenames [i] = xstrdup (filename);
+	}
+    }
+  else
+    {
+      char *p = start_of_files;
+      for ( ; *p; count ++, p += strlen (p) + 1)
+	;
+      filenames = malloc ((count + 1) * sizeof (char*));
+      filenames [count] = NULL;
+      p = start_of_files;
+      for ( int i = 0; *p; p += strlen(p) + 1, i++)
+	{
+	  filename_from_ansi (p, filename);
+	  filenames[i] = xstrdup (filename);
+	}
+    }
+  return filenames;
+}
+
+struct w32_drop_target {
+  /* i_drop_target must be the first member.  */
+  IDropTarget i_drop_target;
+  HWND hwnd;
+};
+
+static HRESULT STDMETHODCALLTYPE
+w32_drop_target_QueryInterface (IDropTarget *t, REFIID ri, void **r)
+{
+  return E_NOINTERFACE;
+}
+
+static ULONG STDMETHODCALLTYPE
+w32_drop_target_AddRef (IDropTarget *This)
+{
+  return 1;
+}
+
+static ULONG STDMETHODCALLTYPE
+w32_drop_target_Release (IDropTarget *This)
+{
+  struct w32_drop_target *target = (struct w32_drop_target *)This;
+  xfree( target->i_drop_target.lpVtbl );
+  xfree( target );
+  return 0;
+}
+
+static HRESULT STDMETHODCALLTYPE
+w32_drop_target_DragEnter( IDropTarget *This, IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
+{
+  *pdwEffect = DROPEFFECT_COPY;
+  return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE
+w32_drop_target_DragOver ( IDropTarget *This, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
+{
+  *pdwEffect = DROPEFFECT_COPY;
+  return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE
+w32_drop_target_DragLeave( IDropTarget *This )
+{
+  return S_OK;
+}
+
+static HRESULT STDMETHODCALLTYPE
+w32_drop_target_Drop (IDropTarget *This, IDataObject *pDataObj,
+		      DWORD grfKeyState, POINTL pt, DWORD *pdwEffect)
+{
+  struct w32_drop_target *target = (struct w32_drop_target *)This;
+  *pdwEffect = DROPEFFECT_COPY;
+
+  W32Msg msg = {0};
+  msg.dwModifiers = w32_get_modifiers();
+  msg.msg.time = GetMessageTime ();
+  msg.msg.pt.x = pt.x;
+  msg.msg.pt.y = pt.y;
+
+  STGMEDIUM stgmedium;
+
+  /* Try for dropped files (CF_HDROP).  */
+  FORMATETC formatetc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
+  if (SUCCEEDED (IDataObject_GetData (pDataObj, &formatetc, &stgmedium)))
+    {
+      if (stgmedium.tymed == TYMED_HGLOBAL)
+	{
+	  DROPFILES *files = (DROPFILES*)GlobalLock (stgmedium.hGlobal);
+	  if (files)
+	      my_post_msg (&msg, target->hwnd, WM_EMACS_DROPFILES,
+			   0, (LPARAM)process_dropfiles (files) );
+	  GlobalUnlock (stgmedium.hGlobal);
+	}
+      ReleaseStgMedium (&stgmedium);
+      return S_OK;
+    }
+
+  formatetc.cfFormat = CF_UNICODETEXT;
+  if (SUCCEEDED (IDataObject_GetData (pDataObj, &formatetc, &stgmedium)))
+    {
+      if (stgmedium.tymed == TYMED_HGLOBAL)
+	{
+	  WCHAR *text = (WCHAR*)GlobalLock (stgmedium.hGlobal);
+	  Lisp_Object text_string = from_unicode_buffer (text);
+	  char *utf8 = xstrdup (SSDATA (ENCODE_UTF_8 (text_string)));
+	  my_post_msg (&msg, target->hwnd, WM_EMACS_DROPSTRING,
+		       0, (LPARAM)utf8 );
+	  GlobalUnlock (stgmedium.hGlobal);
+	}
+      ReleaseStgMedium (&stgmedium);
+      return S_OK;
+    }
+
+  formatetc.cfFormat = CF_TEXT;
+  if (SUCCEEDED (IDataObject_GetData (pDataObj, &formatetc, &stgmedium)))
+    {
+      if (stgmedium.tymed == TYMED_HGLOBAL)
+	{
+	  char *text = (char*)GlobalLock (stgmedium.hGlobal);
+
+	  int l = strlen (text);
+	  WCHAR *text_utf16 = xmalloc (sizeof (WCHAR) * l + 1 );;
+	  if (MultiByteToWideChar (CP_ACP, 0, text, l, text_utf16, l + 1))
+	    {
+	      Lisp_Object text_string = from_unicode_buffer (text_utf16);
+	      char *utf8 = xstrdup (SSDATA (ENCODE_UTF_8 (text_string)));
+	      my_post_msg (&msg, target->hwnd, WM_EMACS_DROPSTRING,
+			   0, (LPARAM)utf8 );
+	    }
+	  xfree (text_utf16);
+	  GlobalUnlock (stgmedium.hGlobal);
+	}
+      ReleaseStgMedium (&stgmedium);
+      return S_OK;
+    }
+
+  return S_OK;
+}
+
 static void
 w32_createwindow (struct frame *f, int *coords)
 {
@@ -2548,7 +2720,23 @@ w32_createwindow (struct frame *f, int *coords)
       SetWindowLong (hwnd, WND_BACKGROUND_INDEX, FRAME_BACKGROUND_PIXEL (f));
 
       /* Enable drag-n-drop.  */
+      struct w32_drop_target *drop_target = xmalloc (sizeof (struct w32_drop_target));
+      drop_target-> hwnd = hwnd;
+
+      IDropTargetVtbl *vtbl = xmalloc (sizeof (IDropTargetVtbl));
+      drop_target->i_drop_target.lpVtbl = vtbl;
+      vtbl->QueryInterface = w32_drop_target_QueryInterface;
+      vtbl->AddRef = w32_drop_target_AddRef;
+      vtbl->Release = w32_drop_target_Release;
+      vtbl->DragEnter = w32_drop_target_DragEnter;
+      vtbl->DragOver = w32_drop_target_DragOver;
+      vtbl->DragLeave = w32_drop_target_DragLeave;
+      vtbl->Drop = w32_drop_target_Drop;
+      RegisterDragDrop (hwnd, &drop_target->i_drop_target);
+
+#if 0
       DragAcceptFiles (hwnd, TRUE);
+#endif
 
       /* Enable system light/dark theme.  */
       w32_applytheme (hwnd);
@@ -3399,6 +3587,8 @@ #define M(msg) { msg, # msg }
       M (WM_EMACS_PAINT),
       M (WM_EMACS_IME_STATUS),
       M (WM_CHAR),
+      M (WM_EMACS_DROPFILES),
+      M (WM_EMACS_DROPSTRING),
 #undef M
       { 0, 0 }
   };
@@ -3465,13 +3655,14 @@ w32_msg_pump (deferred_msg * msg_buf)
 	      /* Produced by complete_deferred_msg; just ignore.  */
 	      break;
 	    case WM_EMACS_CREATEWINDOW:
-	      /* Initialize COM for this window. Even though we don't use it,
-		 some third party shell extensions can cause it to be used in
+	      /* Initialize COM for this window. Needed for RegisterDragDrop.
+		 Some third party shell extensions can cause it to be used in
 		 system dialogs, which causes a crash if it is not initialized.
 		 This is a known bug in Windows, which was fixed long ago, but
 		 the patch for XP is not publicly available until XP SP3,
 		 and older versions will never be patched.  */
-	      CoInitialize (NULL);
+	      OleInitialize (NULL);
+
 	      w32_createwindow ((struct frame *) msg.wParam,
 				(int *) msg.lParam);
 	      if (!PostThreadMessage (dwMainThreadId, WM_EMACS_DONE, 0, 0))
@@ -5106,7 +5297,9 @@ #define MOUSEEVENTF_FROMTOUCH 0xFF515700
       return 0;
 
     case WM_MOUSEWHEEL:
+#if 0
     case WM_DROPFILES:
+#endif
       wmsg.dwModifiers = w32_get_modifiers ();
       my_post_msg (&wmsg, hwnd, msg, wParam, lParam);
       signal_user_input ();
@@ -5597,7 +5790,10 @@ #define WM_TOUCH 576
       }
 
     case WM_EMACS_DESTROYWINDOW:
+	RevokeDragDrop ((HWND) wParam);
+#if 0
       DragAcceptFiles ((HWND) wParam, FALSE);
+#endif
       return DestroyWindow ((HWND) wParam);
 
     case WM_EMACS_HIDE_CARET:
diff --git a/src/w32term.c b/src/w32term.c
index 62037e3b2cd..e1ff4112a7b 100644
--- a/src/w32term.c
+++ b/src/w32term.c
@@ -3576,6 +3576,7 @@ w32_construct_mouse_wheel (struct input_event *result, W32Msg *msg,
   return Qnil;
 }
 
+#if 0
 static Lisp_Object
 w32_construct_drag_n_drop (struct input_event *result, W32Msg *msg,
                            struct frame *f)
@@ -3651,6 +3652,7 @@ w32_construct_drag_n_drop (struct input_event *result, W32Msg *msg,
   return Qnil;
 }
 
+#endif
 
 #if HAVE_W32NOTIFY
 
@@ -5682,12 +5684,56 @@ w32_read_socket (struct terminal *terminal,
 	  }
 	  break;
 
+#if 0
 	case WM_DROPFILES:
 	  f = w32_window_to_frame (dpyinfo, msg.msg.hwnd);
 
 	  if (f)
 	    w32_construct_drag_n_drop (&inev, &msg, f);
 	  break;
+#endif
+
+	case WM_EMACS_DROPSTRING:
+	    f = w32_window_to_frame (dpyinfo, msg.msg.hwnd);
+	    if (!f)
+	      break;
+	    XSETFRAME (inev.frame_or_window, f);
+	    inev.kind = DRAG_N_DROP_EVENT;
+	    inev.code = 0;
+	    inev.timestamp = msg.msg.time;
+	    inev.modifiers = msg.dwModifiers;
+	    ScreenToClient (msg.msg.hwnd, &msg.msg.pt);
+	    XSETINT (inev.x, msg.msg.pt.x);
+	    XSETINT (inev.y, msg.msg.pt.y);
+	    inev.arg = make_string ((char*)msg.msg.lParam, strlen((char*)msg.msg.lParam));
+	    xfree ((void*)msg.msg.lParam);
+	    break;
+
+	case WM_EMACS_DROPFILES:
+	  {
+	    f = w32_window_to_frame (dpyinfo, msg.msg.hwnd);
+	    if (!f)
+	      break;
+	    XSETFRAME (inev.frame_or_window, f);
+	    inev.kind = DRAG_N_DROP_EVENT;
+	    inev.code = 0;
+	    inev.timestamp = msg.msg.time;
+	    inev.modifiers = msg.dwModifiers;
+	    ScreenToClient (msg.msg.hwnd, &msg.msg.pt);
+	    XSETINT (inev.x, msg.msg.pt.x);
+	    XSETINT (inev.y, msg.msg.pt.y);
+
+	    Lisp_Object files = Qnil;
+	    char **filenames = (char**)msg.msg.lParam;
+	    for (int n = 0; filenames[n]; n++ )
+	      {
+		files = Fcons (DECODE_FILE (build_string (filenames[n])), files );
+		xfree (filenames[n]);
+	      }
+	    xfree (filenames);
+	    inev.arg = files;
+	    break;
+	  }
 
 	case WM_HSCROLL:
 	  {
diff --git a/src/w32term.h b/src/w32term.h
index 47be542f570..c1253af47c4 100644
--- a/src/w32term.h
+++ b/src/w32term.h
@@ -710,7 +710,9 @@ #define WM_EMACS_BRINGTOTOP            (WM_EMACS_START + 23)
 #define WM_EMACS_INPUT_READY           (WM_EMACS_START + 24)
 #define WM_EMACS_FILENOTIFY            (WM_EMACS_START + 25)
 #define WM_EMACS_IME_STATUS            (WM_EMACS_START + 26)
-#define WM_EMACS_END                   (WM_EMACS_START + 27)
+#define WM_EMACS_DROPFILES             (WM_EMACS_START + 27)
+#define WM_EMACS_DROPSTRING            (WM_EMACS_START + 28)
+#define WM_EMACS_END                   (WM_EMACS_START + 29)
 
 #define WND_FONTWIDTH_INDEX    (0)
 #define WND_LINEHEIGHT_INDEX   (4)
-- 
2.35.1.windows.2