]> git.ipfire.org Git - thirdparty/open-vm-tools.git/commitdiff
Add GTK4 support in open-vm-tools
authorKruti Pendharkar <kp025370@broadcom.com>
Mon, 1 Dec 2025 07:06:48 +0000 (23:06 -0800)
committerKruti Pendharkar <kp025370@broadcom.com>
Mon, 1 Dec 2025 07:06:48 +0000 (23:06 -0800)
19 files changed:
open-vm-tools/configure.ac
open-vm-tools/services/plugins/desktopEvents/x11Lock.c
open-vm-tools/services/plugins/dndcp/Makefile.am
open-vm-tools/services/plugins/dndcp/copyPasteCompatX11.c
open-vm-tools/services/plugins/dndcp/copyPasteCompatX11GTK4.c [new file with mode: 0644]
open-vm-tools/services/plugins/dndcp/copyPasteDnDX11.cpp
open-vm-tools/services/plugins/dndcp/copyPasteUIX11.cpp
open-vm-tools/services/plugins/dndcp/copyPasteUIX11.h
open-vm-tools/services/plugins/dndcp/copyPasteUIX11GTK4.cpp [new file with mode: 0644]
open-vm-tools/services/plugins/dndcp/dnd/dnd.h
open-vm-tools/services/plugins/dndcp/dndGuestBase/copyPasteDnDX11.h
open-vm-tools/services/plugins/dndcp/dndGuestBase/dndUIX11GTK4.h [new file with mode: 0644]
open-vm-tools/services/plugins/dndcp/dndGuestBase/dragDetWndX11GTK4.h [new file with mode: 0644]
open-vm-tools/services/plugins/dndcp/dndGuestBase/guestDnD.hh
open-vm-tools/services/plugins/dndcp/dndPluginIntX11.h
open-vm-tools/services/plugins/dndcp/dndUIX11GTK4.cpp [new file with mode: 0644]
open-vm-tools/services/plugins/dndcp/dragDetWndX11GTK4.cpp [new file with mode: 0644]
open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.cc [new file with mode: 0644]
open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.hh [new file with mode: 0644]

index 38b8f6c0e9e8d30b052354f3e63164b573c9996c..598df55e65cce04cca2239217dba71654cbe3a47 100644 (file)
@@ -230,6 +230,14 @@ AC_ARG_ENABLE(
    [enable_multimon="$enableval"],
    [enable_multimon="yes"])
 
+AC_ARG_WITH(
+   gtk4,
+   AS_HELP_STRING(
+      [--without-gtk4],
+      [compiles without Gtk 4.0]),
+   [with_gtk4="$withval"],
+   [with_gtk4="auto"])
+
 AC_ARG_WITH(
    gtk3,
    AS_HELP_STRING(
@@ -247,60 +255,41 @@ AC_ARG_WITH(
    [with_gtk2="auto"])
 
 
+# only one "with_gtkX" option allowed
+num_gtk_args=0
+if test "$with_gtk4" = "yes" ; then
+   (( num_gtk_args += 1 ))
+fi
+if test "$with_gtk3" = "yes" ; then
+   (( num_gtk_args += 1 ))
+fi
+if test "$with_gtk2" = "yes" ; then
+   (( num_gtk_args += 1 ))
+fi
+if (( $num_gtk_args > 1 )); then
+   AC_MSG_ERROR('cannot set multiple "with-gtkX" option')
+fi
+
+# any "with_gtkX option is ignored if "no_x" is set
 if test "$no_x" = "yes" ; then
    with_gtk2="no"
    with_gtk3="no"
-else
-   if test "$with_gtk2" = "auto" ; then
-      if test "$with_gtk3" = "auto" ; then
-         with_gtk2="no"
-         with_gtk3="yes"
-      elif test "$with_gtk3" = "no" ; then
-         with_gtk2="yes"
-      elif test "$with_gtk3" = "yes" ; then
-         with_gtk2="no"
-      fi
-   elif test "$with_gtk2" = "no" ; then
-      if test "$with_gtk3" = "auto" ; then
-         with_gtk3="yes"
-      elif test "$with_gtk3" = "no" ; then
-         AC_MSG_ERROR('need either gtk2 or gtk3 or build with --without-x to disable building desktop plugins')
-      fi
-   elif test "$with_gtk2" = "yes"  ; then
-      if test "$with_gtk3" = "auto" ; then
-         with_gtk3="no"
-      elif test "$with_gtk3" = "yes" ; then
-         AC_MSG_ERROR('cannot set both --with-gtk2 and --with-gtk3')
-      fi
+   with_gtk4="no"
+   if (( $num_gtk_args > 0 )); then
+      AC_MSG_WARN('"with-gtkX" option is ignored when "no_x" option is set')
    fi
 fi
 
-if test "$with_gtk2" = "no" ; then
-   with_gtkmm="no"
-fi
-
-if test "$with_gtk3" = "no" ; then
-   with_gtkmm3="no"
-fi
-
-if test "$with_gtk3" = "yes"; then
-   AC_ARG_WITH(
-      gtkmm3,
-      AS_HELP_STRING(
-         [--without-gtkmm3],
-         [compiles without Gtkmm 3, sigc++, and related libs]),
-      [with_gtkmm3="$withval"],
-      [with_gtkmm3="yes"])
-      with_gtkmm="no"
-elif test "$with_gtk2" = "yes"; then
-   AC_ARG_WITH(
-      gtkmm,
-      AS_HELP_STRING(
-         [--without-gtkmm],
-         [compiles without Gtkmm, sigc++, and related libs]),
-      [with_gtkmm="$withval"],
-      [with_gtkmm="yes"])
-      with_gtkmm3="no"
+# use the gtk version if the option is set with "yes" explicitly
+if test "$with_gtk4" = "yes" ; then
+   with_gtk2="no"
+   with_gtk3="no"
+elif test "$with_gtk3" = "yes" ; then
+   with_gtk2="no"
+   with_gtk4="no"
+elif test "$with_gtk2" = "yes" ; then
+   with_gtk3="no"
+   with_gtk4="no"
 fi
 
 AC_ARG_ENABLE(
@@ -1017,6 +1006,32 @@ else
       have_xcomposite="yes"
    fi
 
+   # Check if the gtk lib installed matches with gtkN option
+   have_gtk="no"
+   # Check whether we have gtk+ 4.0
+   if test "$with_gtk4" != "no"; then
+      # gdk_x11_display_get_default_group (added in gtk+ 4.0) is function currently
+      # needed by vmware-user.
+      AC_VMW_CHECK_LIB([gtk-4],
+                       [GTK],
+                       [gtk4],
+                       [],
+                       [4.0.0],
+                       [gtk/x11/gtkx.h],
+                       [gdk_x11_display_get_default_group],
+                       [with_gtk4="yes"
+                        have_gtk="yes"
+                        GTK_CPPFLAGS="$GTK_CPPFLAGS -DGTK4 -DGDK_DISABLE_DEPRECATED -DGTK_DISABLE_DEPRECATED"
+                        with_gtk3="no"
+                        with_gtk2="no"],
+                       [AS_IF([test $with_gtk4 = "auto"],
+                              [AC_MSG_WARN([Gtk+ 4.0 library not found or devel package not found. Please configure without Gtk+ support (using --without-gtk4) or install the Gtk+ 4.0 devel package.])
+                               with_gtk4="no"],
+                              [AC_MSG_ERROR([Gtk+ 4.0 library not found or devel package not found. Please configure without Gtk+ support (using --without-gtk4) or install the Gtk+ 4.0 devel package.])]
+                             )]
+                      )
+   fi
+
    # Check whether we have gtk+ 3.0.
    if test "$with_gtk3" != "no"; then
       # gdk_display_get_default_group (added in gtk+ 2.4.0) is function currently
@@ -1028,11 +1043,20 @@ else
                        [3.0.0],
                        [gtk/gtk.h],
                        [gdk_display_get_default_group],
-                       [GTK_CPPFLAGS="$GTK_CPPFLAGS -DGTK3"],
-                       [AC_MSG_ERROR([Gtk+ 3.0 library not found or too old. Please configure without Gtk+ support (using --without-gtk3) or install the Gtk+ 3.0 devel package.])])
+                       [with_gtk3="yes"
+                        have_gtk="yes"
+                        GTK_CPPFLAGS="$GTK_CPPFLAGS -DGTK3"
+                        with_gtk2="no"],
+                       [AS_IF([test $with_gtk3 = "auto"],
+                              [AC_MSG_WARN([Gtk+ 3.0 library not found or devel package not found. Please configure without Gtk+ support (using --without-gtk3) or install the Gtk+ 3.0 devel package.])
+                               with_gtk3="no"],
+                              [AC_MSG_ERROR([Gtk+ 3.0 library not found or devel package not found. Please configure without Gtk+ support (using --without-gtk3) or install the Gtk+ 3.0 devel package.])]
+                             )]
+                      )
+   fi
 
    # Check whether we have gtk+ 2.0.
-   elif test "$with_gtk2" != "no"; then
+   if test "$with_gtk2" != "no"; then
       AC_VMW_CHECK_LIB([gtk-x11-2.0],
                        [GTK],
                        [gtk+-2.0],
@@ -1040,14 +1064,81 @@ else
                        [2.4.0],
                        [gtk/gtk.h],
                        [gdk_display_get_default_group],
-                       [GTK_CPPFLAGS="$GTK_CPPFLAGS -DGTK2"],
-                       [AC_MSG_ERROR([Gtk+ 2.0 library not found or too old. Please configure without Gtk+ support (using --without-gtk2) or install the Gtk+ 2.0 devel package.])])
+                       [with_gtk2="yes"
+                        have_gtk="yes"
+                        GTK_CPPFLAGS="$GTK_CPPFLAGS -DGTK2"],
+                       [AS_IF([test $with_gtk2 = "auto"],
+                              [AC_MSG_WARN([Gtk+ 2.0 library not found or devel package not found. Please configure without Gtk+ support (using --without-gtk2) or install the Gtk+ 2.0 devel package.])
+                               with_gtk2="no"],
+                              [AC_MSG_ERROR([Gtk+ 2.0 library not found or devel package not found. Please configure without Gtk+ support (using --without-gtk2) or install the Gtk+ 2.0 devel package.])]
+                             )]
+                      )
+   fi
+
+   if test "$have_gtk" != "yes" && test "$no_x" != "yes"; then
+      AC_MSG_ERROR('need at least one of gtk2/gtk3/gtk4 available or build with --without-x to disable building desktop plugins')
+   fi
+
+   # Now set gtkmm version after gtkN version is determined
+   if test "$with_gtk4" = "yes"; then
+      AC_ARG_WITH(
+         gtkmm4,
+         AS_HELP_STRING(
+            [--without-gtkmm4],
+            [compiles without Gtkmm 4, sigc++, and related libs]),
+         [with_gtkmm4="$withval"],
+         [with_gtkmm4="yes"])
+         with_gtkmm="no"
+         with_gtkmm3="no"
+   elif test "$with_gtk3" = "yes"; then
+      AC_ARG_WITH(
+         gtkmm3,
+         AS_HELP_STRING(
+            [--without-gtkmm3],
+            [compiles without Gtkmm 3, sigc++, and related libs]),
+         [with_gtkmm3="$withval"],
+         [with_gtkmm3="yes"])
+         with_gtkmm="no"
+         with_gtkmm4="no"
+   elif test "$with_gtk2" = "yes"; then
+      AC_ARG_WITH(
+         gtkmm,
+         AS_HELP_STRING(
+            [--without-gtkmm],
+            [compiles without Gtkmm, sigc++, and related libs]),
+         [with_gtkmm="$withval"],
+         [with_gtkmm="yes"])
+         with_gtkmm3="no"
+         with_gtkmm4="no"
    fi
 
    #
    # Check for gtkmm 2.4.0 or greater.
    #
-   if test "$with_gtkmm" != "no" -o "$with_gtkmm3" != "no"; then
+   if test "$with_gtkmm4" != "no"; then
+      AC_VMW_CHECK_LIBXX([gtkmm-4.0],
+                         [GTKMM],
+                         [gtkmm-4.0],
+                         [],
+                         [4.0.0],
+                         [gtkmm.h],
+                         [],
+                         [GTKMM_CPPFLAGS="$GTKMM_CPPFLAGS -DHAVE_GTKMM"],
+                         [AC_MSG_ERROR([gtkmm4 library not found. Please install the libgtkmm4 devel package(s), or re-configure using --without-gtkmm4.])])
+
+      #
+      # libsigc++-3.0 >= 3.0.0 requires C++17 support
+      #
+      AC_VMW_CHECK_LIBXX([sigc++-3.0],
+                         [SIGCXX],
+                         [sigc++-3.0],
+                         [],
+                         [3.0.0],
+                         [sigc++.h],
+                         [],
+                         [SIGCXX_CPPFLAGS="$SIGCXX_CPPFLAGS -std=c++17"],
+                         [SIGCXX_CPPFLAGS="$SIGCXX_CPPFLAGS"])
+   elif test "$with_gtkmm" != "no" -o "$with_gtkmm3" != "no"; then
       if test "$with_gtkmm3" != "no"; then
          AC_VMW_CHECK_LIBXX([gtkmm-3.0],
                             [GTKMM],
@@ -1636,6 +1727,12 @@ if test "x$enable_vmwgfxctrl" = "xauto"; then
    enable_vmwgfxctrl="no"
 fi
 
+if test "$with_gtkmm" = "yes" || test "$with_gtkmm3" = "yes" || test "$with_gtkmm4" = "yes"; then
+   have_gtkmm="yes"
+else
+   have_gtkmm="no"
+   AC_MSG_WARN('dndcp plugin will not build without gtkmm')
+fi
 AM_CONDITIONAL(LINUX, test "$os" = "linux")
 AM_CONDITIONAL(SOLARIS, test "$os" = "solaris")
 AM_CONDITIONAL(FREEBSD, test "$os" = "freebsd" -o "$os" = "kfreebsd-gnu")
@@ -1655,7 +1752,8 @@ AM_CONDITIONAL(HAVE_DOXYGEN, test "$have_doxygen" = "yes")
 AM_CONDITIONAL(HAVE_FUSE, test "$have_fuse" = "yes" || test "$have_fuse3" = "yes")
 AM_CONDITIONAL(HAVE_FUSE3, test "$have_fuse3" = "yes")
 AM_CONDITIONAL(HAVE_GNU_LD, test "$with_gnu_ld" = "yes")
-AM_CONDITIONAL(HAVE_GTKMM, test "$have_x" = "yes" -a \( "$with_gtkmm" = "yes" -o "$with_gtkmm3" = "yes" \) )
+AM_CONDITIONAL(HAVE_GTK4,  test "$have_x" = "yes" && test "$with_gtk4" = "yes")
+AM_CONDITIONAL(HAVE_GTKMM,  test "$have_x" = "yes" && test "$have_gtkmm" = "yes")
 AM_CONDITIONAL(HAVE_PAM, test "$with_pam" = "yes")
 AM_CONDITIONAL(USE_SLASH_PROC, test "$os" = "linux")
 AM_CONDITIONAL(ENABLE_CONTAINERINFO, test "$enable_containerinfo" = "yes")
index 53b44bde11337f0079dc5f3ece187300fb2f43a7..3e7e5c8c5c40c6195db8d7ea47b4d276ba65d67d 100644 (file)
 
 #include <stdlib.h>
 #include <string.h>
-#include <gdk/gdkx.h>
 #include <gtk/gtk.h>
 #include <X11/Xlib.h>
+#if GTK_MAJOR_VERSION == 4
+#include <gdk/x11/gdkx.h>
+#else
+#include <gdk/gdkx.h>
+#endif
 
 #define LOCK_ATOM_NAME  "vmware-user-lock"
+#if GTK_MAJOR_VERSION == 4
+#define GDK_GET_XDISPLAY(X) gdk_x11_display_get_xdisplay(X)
+#else
+#define GDK_GET_XDISPLAY(X) gdk_x11_get_default_xdisplay()
+#endif
 
 
 /*
@@ -74,7 +83,11 @@ InitGroupLeader(Window *groupLeader,
    Window myRootWindow;
    XSetWindowAttributes attr;
    GdkDisplay *gdkDisplay;
+#if GTK_MAJOR_VERSION == 4
+   GdkSurface *gdkLeader;
+#else
    GdkWindow *gdkLeader;
+#endif
 
    attr.override_redirect = True;
 
@@ -82,15 +95,25 @@ InitGroupLeader(Window *groupLeader,
    ASSERT(rootWindow);
 
    gdkDisplay = gdk_display_get_default();
+#if GTK_MAJOR_VERSION == 4
+   gdkLeader = gdk_x11_display_get_default_group(gdkDisplay);
+   myGroupLeader = gdk_x11_surface_get_xid(gdkLeader);
+   /*
+    * GTK4 deprecated the idea of root windows
+    * https://docs.gtk.org/gtk4/migrating-3to4.html#stop-using-the-root-window
+    */
+   myRootWindow = gdk_x11_display_get_xrootwindow(gdkDisplay);
+#else
    gdkLeader = gdk_display_get_default_group(gdkDisplay);
    myGroupLeader = GDK_WINDOW_XID(gdkLeader);
    myRootWindow = GDK_ROOT_WINDOW();
+#endif
 
    ASSERT(myGroupLeader);
    ASSERT(myRootWindow);
 
    /* XXX: With g_set_prgname() being called, this can probably go away. */
-   XStoreName(gdk_x11_get_default_xdisplay(), myGroupLeader, VMUSER_TITLE);
+   XStoreName(GDK_GET_XDISPLAY(gdkDisplay), myGroupLeader, VMUSER_TITLE);
 
    /*
     * Confidence check:  Set the override redirect property on our group leader
@@ -98,10 +121,11 @@ InitGroupLeader(Window *groupLeader,
     * This makes sure that (a) a window manager can't re-parent our window,
     * and (b) that we remain a top-level window.
     */
-   XChangeWindowAttributes(gdk_x11_get_default_xdisplay(), myGroupLeader, CWOverrideRedirect,
-                           &attr);
-   XReparentWindow(gdk_x11_get_default_xdisplay(), myGroupLeader, myRootWindow, 10, 10);
-   XSync(gdk_x11_get_default_xdisplay(), FALSE);
+   XChangeWindowAttributes(GDK_GET_XDISPLAY(gdkDisplay), myGroupLeader,
+                           CWOverrideRedirect, &attr);
+   XReparentWindow(GDK_GET_XDISPLAY(gdkDisplay), myGroupLeader, myRootWindow,
+                   10, 10);
+   XSync(GDK_GET_XDISPLAY(gdkDisplay), FALSE);
 
    *groupLeader = myGroupLeader;
    *rootWindow = myRootWindow;
@@ -253,8 +277,14 @@ AcquireDisplayLock(void)
    unsigned int index;
    Bool alreadyLocked = FALSE;  // Set to TRUE if we discover lock is held.
    Bool retval = FALSE;
+   GdkDisplay *gdkDisplay;
 
-   defaultDisplay = gdk_x11_get_default_xdisplay();
+#if GTK_MAJOR_VERSION == 4
+   gdkDisplay = gdk_display_get_default();
+#else
+   gdkDisplay = NULL;
+#endif
+   defaultDisplay = GDK_GET_XDISPLAY(gdkDisplay);
 
    /*
     * Reset some of our main window's settings & fetch Xlib handles for
@@ -391,7 +421,6 @@ gboolean
 X11Lock_Init(ToolsAppCtx *ctx,
              ToolsPluginData *pdata)
 {
-   int argc = 0;
    char *argv[] = { NULL, NULL };
 
    if (!TOOLS_IS_USER_SERVICE(ctx)) {
@@ -422,7 +451,12 @@ X11Lock_Init(ToolsAppCtx *ctx,
    gdk_set_allowed_backends("x11");
 #endif
    /* XXX: is calling gtk_init() multiple times safe? */
+#if GTK_MAJOR_VERSION == 4
+   gtk_init();
+#else
+   int argc = 0;
    gtk_init(&argc, (char ***) &argv);
+#endif
 
    if (!AcquireDisplayLock()) {
       g_warning("Another instance of vmware-user already running. Exiting.\n");
index 5a864e7c065b4460f064d902e1beab1ccc34ec30..e093874592510010faee0dd80ae260d1fc028f0d 100644 (file)
@@ -86,7 +86,11 @@ libdndcp_la_SOURCES += dndGuest/rpcV4Util.cpp
 libdndcp_la_SOURCES += dndGuest/dndCPTransportGuestRpc.cpp
 
 libdndcp_la_SOURCES += stringxx/string.cc
+if HAVE_GTK4
+libdndcp_la_SOURCES += xutils/xutilsGTK4.cc
+else
 libdndcp_la_SOURCES += xutils/xutils.cc
+endif
 
 libdndcp_la_SOURCES += dndcp.cpp
 libdndcp_la_SOURCES += pointer.cpp
@@ -94,10 +98,17 @@ libdndcp_la_SOURCES += copyPasteCompat.c
 libdndcp_la_SOURCES += vmCopyPasteDnDWrapper.cpp
 libdndcp_la_SOURCES += copyPasteDnDX11.cpp
 libdndcp_la_SOURCES += copyPasteDnDUtil.cpp
+if HAVE_GTK4
+libdndcp_la_SOURCES += copyPasteCompatX11GTK4.c
+libdndcp_la_SOURCES += copyPasteUIX11GTK4.cpp
+libdndcp_la_SOURCES += dndUIX11GTK4.cpp
+libdndcp_la_SOURCES += dragDetWndX11GTK4.cpp
+else
 libdndcp_la_SOURCES += copyPasteCompatX11.c
 libdndcp_la_SOURCES += copyPasteUIX11.cpp
 libdndcp_la_SOURCES += dndUIX11.cpp
 libdndcp_la_SOURCES += dragDetWndX11.cpp
+endif
 
 if LINUX
 libdndcp_la_CPPFLAGS += -I$(top_srcdir)/services/plugins/dndcp/fakeMouse
index 40e295a8d2c56c2ff2b6d95338d796751661f241..337621efab58375f6174e14e15d0035107916313 100644 (file)
@@ -40,6 +40,9 @@
  *    selection text.
  */
 
+#  if !defined(GTK2) && !defined(GTK3)
+#  error "This should only build with GTK2 or GTK3"
+#  endif
 #define G_LOG_DOMAIN "dndcp"
 
 #include "dndPluginIntX11.h"
diff --git a/open-vm-tools/services/plugins/dndcp/copyPasteCompatX11GTK4.c b/open-vm-tools/services/plugins/dndcp/copyPasteCompatX11GTK4.c
new file mode 100644 (file)
index 0000000..643d2c5
--- /dev/null
@@ -0,0 +1,482 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+/*
+ * copyPasteCompatX11GTK4.c --
+ *
+ *    Like copyPasteCompatX11.c, this is the GTK4 version implementation for
+ *    Linux OS for guest<-->host text copy/paste. We need separate
+ *    implementation for GTK4 as its API changed dramaticly compared to GTK3.
+ *
+ *    Set of functions in this file are for guest side text copy/paste between
+ *    host and guest. Currently there are 4 versions copy/paste.
+ *    ** Version 1 is based on backdoor, supports only text copy/paste,
+ *    available with GTK2/GTK3/GTK4.
+ *    ** Version 2/3/4 are all based on tools guestRPC.
+ *    *** Version 2 only existed for a certain period for compatibility with
+ *    pre-2000 windows guest and is deprecated.
+ *    *** Version 3 also existed for compatibility with some old tools with
+ *    Fusion 3, WS 7 etc and is considered deprecated now.
+ *    *** Version 4 is the current version, which supports both text and file
+ *    copy/paste.
+ *    Refer to "bora/vmx/tools/dndController" for more details.
+ *
+ *    ### G->H Text Copy/Paste (version 1) ###
+ *    --------------------
+ *    When Ungrab, CopyPaste_RequestSelection got called, which try to get
+ *    selection text and send to backdoor.
+ *
+ *    ### H->G Text Copy/Paste (version 1) ###
+ *    --------------------
+ *    When grab, CopyPaste_GetBackdoorSelections got called, which first
+ *    get host selection text and set its content in gHostClipboardBuf.
+ *    Then the gGdkDisplay default clipboard is set with its content.
+ */
+#ifndef GTK4
+#error "This should only build with GTK4"
+#endif
+#define G_LOG_DOMAIN "dndcp"
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+
+#include "copyPasteCompat.h"
+#include "dndPluginIntX11.h"
+#include "util.h"
+#include "vm_assert.h"
+#include "vmware/guestrpc/tclodefs.h"
+#include "vmware/tools/plugin.h"
+
+/*
+ * Currently there are 2 active versions copy/paste: version 1 and version 4
+ * (version 2/3  are considered deprecated):
+ * Key points in copy/paste version 1:
+ * 1. Only text copy/paste
+ * 2. copy/paste is based on backdoor directly
+ *
+ * Key points in copy/paste version 4:
+ * 1. Support both file/text copy/paste
+ * 2. Both file/text copy/paste are based on guestRPC
+ */
+static int32 gVmxCopyPasteVersion = 1;
+
+/*
+ * Here gHostClipboardBuf is one byte lager than MAX_SELECTION_BUFFER_LENGTH,
+ * As the backdoor bit copy is 4-byte aligned, we could copy H->G with exactly
+ * MAX_SELECTION_BUFFER_LENGTH(65536 - 100) bytes at most as its value is 4-byte
+ * aligned, we need additional 1 termination char '\0'.
+ * While for the gGuestClipboardBuf, as we are copy G->H also with 4-byte
+ * aligned, we need to include the termination '\0' at the last char, thus it
+ * could be MAX_SELECTION_BUFFER_LENGTH at most.
+ */
+static char gGuestClipboardBuf[MAX_SELECTION_BUFFER_LENGTH];
+static char gHostClipboardBuf[MAX_SELECTION_BUFFER_LENGTH + 1];
+static unsigned int gGuestClipGenNum;
+static unsigned int gHostClipGenNum;
+static gint64 gGuestDefaultClipTime = 0;
+static gint64 gGuestPrimaryClipTime = 0;
+
+static ToolsAppCtx *gCtx = NULL;
+static GdkClipboard *gDefaultClipBd = NULL;
+static GdkClipboard *gPrimaryClipBd = NULL;
+
+/*
+ * Forward Declarations
+ */
+static void CopyPasteSetBackdoorSelections(void);
+static void CopyPasteRequestTextCb(GObject *source_object,
+                                   GAsyncResult *result,
+                                   gpointer user_data);
+static void GuestDefaultClipbdChangedCb(GdkClipboard *clipboard,
+                                        gpointer user_data);
+static void GuestPrimaryClipbdChangedCb(GdkClipboard *clipboard,
+                                        gpointer user_data);
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * GuestDefaultClipbdChangedCb --
+ *
+ *      Callback when the guest Default clipboard changed.
+ *      Either guest own app or the H-G copy could change the guest clipboard.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      If clipboard text changed, gGuestCpGenNum is increased by 1 and
+ *      gGuestDefaultClipTime is updated.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+static void
+GuestDefaultClipbdChangedCb(GdkClipboard *clipboard,   // UNUSED
+                            gpointer user_data)        // UNUSED
+{
+   GdkContentFormats *clipFormats = gdk_clipboard_get_formats(gDefaultClipBd);
+
+   /*
+    * Only update when clipboard text content changed,
+    * as v1 copy/paste only supports plain text
+    */
+   if (gdk_content_formats_contain_gtype(clipFormats, G_TYPE_STRING)) {
+      ++gGuestClipGenNum;
+      gGuestDefaultClipTime = g_get_real_time();
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * GuestPrimaryClipbdChangedCb --
+ *
+ *      Callback when the guest Primary clipboard changed.
+ *      Either guest own app or the H-G copy could change the guest clipboard.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      If clipboard text changed, gGuestCpGenNum is increased by 1 and
+ *      gGuestPrimaryClipTime is updated.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+static void
+GuestPrimaryClipbdChangedCb(GdkClipboard *clipboard,   // UNUSED
+                            gpointer user_data)        // UNUSED
+{
+   GdkContentFormats *clipFormats = gdk_clipboard_get_formats(gPrimaryClipBd);
+
+   /*
+    * Only update when clipboard text content changed,
+    * as v1 copy/paste only supports plain text
+    */
+   if (gdk_content_formats_contain_gtype(clipFormats, G_TYPE_STRING)) {
+      ++gGuestClipGenNum;
+      gGuestPrimaryClipTime = g_get_real_time();
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ *  CopyPasteRequestTextCb--
+ *
+ *      Callback when finish reading the guest's text clipboard, and then send
+ *      the content buffer to host.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      On Success:
+ *      ** gGuestClipboardBuf is set with the guest clipboard text
+ *      ** gGuestClipboardBuf content is sent to host with backdoor.
+ *      ** gHostClipGenNum is set to gGuestClipGenNum.
+ *-----------------------------------------------------------------------------
+ */
+
+static void
+CopyPasteRequestTextCb(GObject *source_object,  // IN/OUT: clipboard
+                       GAsyncResult *result,    // UNUSED
+                       gpointer user_data)      // UNUSED
+{
+   GdkClipboard *clipboard = GDK_CLIPBOARD(source_object);
+   GError *err = NULL;
+   size_t len;
+   /* gdk_clipboard_read_text_finish return NUL terminated UTF-8 string */
+   char *cpStr = gdk_clipboard_read_text_finish(clipboard, result, &err);
+
+   if (cpStr != NULL) {
+      len = strlen(cpStr);
+      if (len >= MAX_SELECTION_BUFFER_LENGTH) {
+          /*
+           * At most MAX_SELECTION_BUFFER_LENGTH-1 size string could be copied
+           * G->H clipboard, as backdoor copy is 4-byte aligned and we need to
+           * keep the last char as '\0' terminator
+           */
+          len = MAX_SELECTION_BUFFER_LENGTH - 1;
+          g_warning("%s: Guest clipboard selection exceeds [%zu] bytes, text truncated\n",
+                    __FUNCTION__, len);
+      }
+      memcpy(gGuestClipboardBuf, cpStr, len);
+      gGuestClipboardBuf[len] = '\0';
+      g_free(cpStr);
+      g_debug("%s: Guest clipboard GenNum=%d, text is [%s]\n", __FUNCTION__,
+              gGuestClipGenNum, gGuestClipboardBuf);
+      CopyPasteSetBackdoorSelections();
+      gHostClipGenNum = gGuestClipGenNum;
+   }
+
+   if (err != NULL) {
+      /* clipboard failure is not critical one,  warn but do not error exit */
+      g_warning("%s: Error to copy from guest clipboard: %s\n", __FUNCTION__,
+                err->message);
+      g_error_free(err);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPaste_RequestSelection --
+ *
+ *      If gHostClipGenNum is different from gGuestClipGenNum, request the
+ *      guest's text clipboard (asynchronously). Once the clipboard read request
+ *      is completed the Callback function is invoked to send the buffer to
+ *      the host.
+ *
+ * Results:
+ *      TRUE on success, FALSE otherwise.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+Bool
+CopyPaste_RequestSelection(void)
+{
+   if (gVmxCopyPasteVersion > 1) {
+      return FALSE;
+   }
+
+   if (gHostClipGenNum == gGuestClipGenNum) {
+      g_debug("%s: Guest clipboard in sync with host with the same GenNum=%d, "
+              "skip send", __FUNCTION__, gGuestClipGenNum);
+   } else {
+      gGuestClipboardBuf[0] = '\0';
+      GdkClipboard *cpClipboard = (gGuestPrimaryClipTime > gGuestDefaultClipTime)
+                                 ? gPrimaryClipBd
+                                 : gDefaultClipBd;
+      gdk_clipboard_read_text_async(cpClipboard, NULL, CopyPasteRequestTextCb, NULL);
+   }
+   return TRUE;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteSetBackdoorSelections --
+ *
+ *      Set the clipboard uses CopyPaste_SetSelLength and set backdoor selection
+ *      with clipboard. If unavailable, set backdoor selection length to be 0.
+ *      Unlike its GTK3/GTK2 implementation in copyPasteCompatX11.c, we could
+ *      not compare with the timestamps so only the default clipboard is used.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      The VMX probably changes some string buffers.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteSetBackdoorSelections(void)
+{
+   uint32 const *p;
+   size_t len = strlen(gGuestClipboardBuf);
+
+   if (len > 0) {
+      p = (uint32 const *)gGuestClipboardBuf;
+      size_t alignedLen;
+      unsigned int i;
+
+      /* Here long string should already be truncated in caller function. */
+      alignedLen = (len + 4) & ~3;
+      ASSERT(alignedLen <= MAX_SELECTION_BUFFER_LENGTH);
+
+      CopyPaste_SetSelLength(len);
+      g_debug("%s: Set host clipboard with [%zu] text.\n", __FUNCTION__, len);
+      for (i = 0; i < len; i += 4, p++) {
+         CopyPaste_SetNextPiece(*p);
+      }
+   } else {
+      CopyPaste_SetSelLength(0);
+      g_debug("%s: Set host clipboard with empty text.\n", __FUNCTION__);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPaste_GetBackdoorSelections --
+ *
+ *      Get the clipboard "the old way".
+ *      The old way uses CopyPaste_GetHostSelectionLen and there's only one
+ *      selection and its content is in gHostClipboardBuf.
+ *
+ *      XXX: the "new way" isn't availble yet because the vmx doesn't
+ *           implement separate clipboards. Even when it does this
+ *           function will still exist for backward compatibility
+ *
+ * Results:
+ *      TRUE if 0 <= selLength <= MAX_SELECTION_BUFFER_LENGTH, FALSE otherwise.
+ *
+ * Side effects:
+ *      * guest clipboard is set with the host's clipboard content.
+ *      * gHostClipGenNum is increased by 1.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+Bool
+CopyPaste_GetBackdoorSelections(void)
+{
+   int selLength;
+
+   if (gVmxCopyPasteVersion > 1) {
+      return TRUE;
+   }
+
+   selLength = CopyPaste_GetHostSelectionLen();
+   if (selLength < 0 || selLength > MAX_SELECTION_BUFFER_LENGTH) {
+      return FALSE;
+   } else if (selLength > 0) {
+      CopyPaste_GetHostSelection(selLength, gHostClipboardBuf);
+      gHostClipboardBuf[selLength] = 0;
+      ++gHostClipGenNum;
+      g_debug("%s: Host clipboard GenNum=%d, text is [%s].\n", __FUNCTION__,
+              gHostClipGenNum, gHostClipboardBuf);
+      /* Initialize a GValue with the contents of the gHostClipboardBuf */
+      GValue value = G_VALUE_INIT;
+
+      g_value_init(&value, G_TYPE_STRING);
+      g_value_set_string(&value, gHostClipboardBuf);
+      /* H->G copy paste, store the value in guest Default and Primary clipboard */
+      gdk_clipboard_set_value(gDefaultClipBd, &value);
+      gdk_clipboard_set_value(gPrimaryClipBd, &value);
+      g_value_unset(&value);
+   }
+   /* finally return TRUE if setLength>=0 */
+   return TRUE;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPaste_Register --
+ *
+ *      Setup callbacks, initialize.
+ *
+ * Results:
+ *      Always TRUE.
+ *
+ * Side effects:
+ *      None
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+Bool
+CopyPaste_Register(GtkWidget* mainWnd,   // IN
+                   ToolsAppCtx *ctx)     // IN
+{
+   g_debug("%s: enter\n", __FUNCTION__);
+   ASSERT(mainWnd);
+   ASSERT(ctx);
+
+   gCtx = ctx;
+   gHostClipboardBuf[0] = '\0';
+   gGuestClipboardBuf[0] = '\0';
+   gGuestClipGenNum = 0;
+   gHostClipGenNum = 0;
+   gDefaultClipBd = gdk_display_get_clipboard(gGdkDisplay);
+   gPrimaryClipBd = gdk_display_get_primary_clipboard(gGdkDisplay);
+
+   g_signal_connect(gDefaultClipBd, "changed",
+                    G_CALLBACK(GuestDefaultClipbdChangedCb), NULL);
+   g_signal_connect(gPrimaryClipBd, "changed",
+                    G_CALLBACK(GuestPrimaryClipbdChangedCb), NULL);
+   return TRUE;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPaste_Unregister --
+ *
+ *      Currently do nothing. Dummy function in GTK4.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPaste_Unregister(GtkWidget* mainWnd)
+{
+   return;
+}
+
+
+/*
+ *----------------------------------------------------------------------------
+ *
+ * CopyPaste_IsRpcCPSupported --
+ *
+ *    Check if RPC copy/paste is supported by vmx or not.
+ *
+ * Results:
+ *    FALSE always.
+ *
+ * Side effects:
+ *    None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+Bool
+CopyPaste_IsRpcCPSupported(void)
+{
+   return gVmxCopyPasteVersion > 1;
+}
+
+
+/**
+ * Set the copy paste version.
+ *
+ * @param[in] version version to set.
+ */
+
+void
+CopyPaste_SetVersion(int version)
+{
+   g_debug("%s: enter version %d\n", __FUNCTION__, version);
+   gVmxCopyPasteVersion = version;
+}
index 4756e0d3856de35277c703f034268ffc197582f8..4082108b1bef93f005349a95a73bad6af374dc18 100644 (file)
 
 #include "copyPasteDnDWrapper.h"
 #include "copyPasteDnDX11.h"
+#ifdef GTK4
+#undef VMTOOLS_USE_LEGACY_GTK
+#include "dndCPMsgV4.h"
+#endif
 #include "copyPasteUIX11.h"
 #include "tracer.hh"
 #include "dndPluginIntX11.h"
@@ -35,6 +39,9 @@
 Window gXRoot;
 Display *gXDisplay;
 GtkWidget *gUserMainWidget;
+#ifdef GTK4
+GdkDisplay *gGdkDisplay;
+#endif
 
 
 extern "C" {
@@ -217,6 +224,9 @@ gboolean
 CopyPasteDnDX11::Init(ToolsAppCtx *ctx)
 {
    TRACE_CALL();
+#if GTK_MAJOR_VERSION > 4
+   g_error("Unsupported GTK version!")
+#endif
 
 #if GTK_MAJOR_VERSION > 3 || (GTK_MAJOR_VERSION == 3 && GTK_MINOR_VERSION >= 10)
    /*
@@ -233,6 +243,9 @@ CopyPasteDnDX11::Init(ToolsAppCtx *ctx)
 
    ASSERT(ctx);
 
+#ifdef GTK4
+   Gtk::Application::create("com.tools.gtk4");
+#endif
 
 #ifdef VMTOOLS_USE_LEGACY_GTK
    int argc = 1;
@@ -243,6 +256,13 @@ CopyPasteDnDX11::Init(ToolsAppCtx *ctx)
    if (wrapper) {
       BlockService::GetInstance()->Init(ctx);
    }
+#ifdef GTK4
+   gUserMainWidget = gtk_window_new();
+   gtk_widget_set_visible(gUserMainWidget, FALSE);
+   gGdkDisplay = gdk_display_get_default();
+   gXDisplay = gdk_x11_display_get_xdisplay(gGdkDisplay);
+   gXRoot = gdk_x11_display_get_xrootwindow(gGdkDisplay);
+#endif
 
 #ifdef VMTOOLS_USE_LEGACY_GTK
    gUserMainWidget = gtk_invisible_new();
@@ -287,6 +307,9 @@ CopyPasteDnDX11::~CopyPasteDnDX11()
 #ifdef VMTOOLS_USE_LEGACY_GTK
    gtk_widget_destroy(gUserMainWidget);
 #endif
+#ifdef GTK4
+   gtk_window_destroy(GTK_WINDOW(gUserMainWidget));
+#endif
 
    }
 }
index 9f3491369c2366f5bc6ec0eae35a413875a08ce1..a4580eeca131b15ddc1dfd4338a3376df39b8a00 100644 (file)
@@ -63,6 +63,9 @@
  *   ignore everything else.
  */
 
+#  if !defined(GTK2) && !defined(GTK3)
+#  error "This could only build with GTK2 or GTK3"
+#  endif
 
 #define G_LOG_DOMAIN "dndcp"
 
index 436b057d6ff9405d3224d6789e6687aeeb33f172..44fab3213384959cce0232e0acff345b9389b77d 100644 (file)
@@ -65,6 +65,10 @@ extern "C" {
 
 #define VMTOOLS_USE_LEGACY_GTK
 
+#ifdef GTK4
+#undef VMTOOLS_USE_LEGACY_GTK
+#include <gdk/x11/gdkx.h>
+#endif
 #ifdef VMTOOLS_USE_LEGACY_GTK
 #include <gdk/gdkx.h>
 #endif
@@ -105,6 +109,18 @@ private:
 
    /* hg */
    void GetRemoteClipboardCB(const CPClipboard *clip);
+#ifdef GTK4
+   void LocalGetFileRequest();
+   void BuildFileURIList(utf::string& uriList,
+                         utf::string pre,
+                         utf::string post,
+                         utf::string stagingDirName);
+   void BuildFileContentURIList(utf::string& uriList,
+                                utf::string pre,
+                                utf::string post);
+   void LocalSetFileListToClipboard(const char* desktop);
+   void LocalGetFileContentsRequest();
+#endif
 
 #ifdef VMTOOLS_USE_LEGACY_GTK
    void LocalGetFileRequestCB(Gtk::SelectionData& selection_data, guint info);
@@ -116,6 +132,20 @@ private:
 
    /* gh */
    void GetLocalClipboard(void);
+#ifdef GTK4
+   void CopyPasteSendText(const Glib::ustring& cpStr);
+   void CopyPasteSendImage(const Glib::RefPtr<Glib::Bytes>& imgData);
+   void CopyPasteSendRtfValue(const Glib::RefPtr<Gio::AsyncResult>& result);
+   void CopyPasteSendFile(const Glib::RefPtr<Gio::AsyncResult>& result);
+   void CopyPasteRequestStreamCb(const Glib::RefPtr<Gio::AsyncResult>& result,
+                                 const Glib::RefPtr<Gdk::Clipboard>& clipboard);
+   void CopyPasteRequestTextCb(const Glib::RefPtr<Gio::AsyncResult>& result,
+                               const Glib::RefPtr<Gdk::Clipboard>& clipboard);
+   void CopyPasteRequestImageCb(const Glib::RefPtr<Gio::AsyncResult>& result,
+                                const Glib::RefPtr<Gdk::Clipboard>& clipboard);
+   void GuestDefaultClipboardChangedCb();
+   void GuestPrimaryClipboardChangedCb();
+#endif
 #ifdef VMTOOLS_USE_LEGACY_GTK
    void LocalClipboardTimestampCB(const Gtk::SelectionData& sd);
    void LocalPrimTimestampCB(const Gtk::SelectionData& sd);
@@ -137,6 +167,11 @@ private:
    GuestCopyPasteMgr *mCP;
    bool mClipboardEmpty;
    utf::string mHGStagingDir;
+#ifdef GTK4
+   Glib::RefPtr<Gdk::Clipboard> mDftClipboardPtr;
+   Glib::RefPtr<Gdk::Clipboard> mPrimClipboardPtr;
+   Glib::RefPtr<Gio::InputStream> mCpStream;
+#endif
 #ifdef VMTOOLS_USE_LEGACY_GTK
    std::vector<Gtk::TargetEntry> mListTargets;
    GdkAtom mGHSelection;
@@ -152,6 +187,11 @@ private:
    /* File vars. */
    VmTimeType mHGGetListTime;
    utf::utf8string mHGFCPData;
+#ifdef GTK4
+   utf::string mHGCopiedUriListGnome;
+   utf::string mHGCopiedUriListKde;
+   utf::string mHGCopiedUriListNautilus;
+#endif
 
 #ifdef VMTOOLS_USE_LEGACY_GTK
    utf::string mHGCopiedUriList;
diff --git a/open-vm-tools/services/plugins/dndcp/copyPasteUIX11GTK4.cpp b/open-vm-tools/services/plugins/dndcp/copyPasteUIX11GTK4.cpp
new file mode 100644 (file)
index 0000000..1ba8db4
--- /dev/null
@@ -0,0 +1,1650 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+/*
+ * copyPasteUIX11GTK4.cpp --
+ *
+ *    Like copyPasteUIX11.c, this is the GTK4 version implementation
+ *    of CopyPaste between host and guest for Linux GuestOS. GTK4 API's
+ *    are completely different than GTK3 hence a seperate implementation
+ *    is needed.
+ *
+ *    Currently there are 2 versions for copy/paste.
+ *    Version 1 is based on backdoor,  supports only text copy/paste.
+ *    Version 4 is based on guestRPC,  supports text, image, rtf and
+ *    file copy/paste.
+ *
+ *    G->H Copy/Paste (version 4)
+ *    --------------------
+ *    When Ungrab, GetLocalClipboard got called, which fetches the
+ *    text, image, rtf or file data from the Clipboard and send it to Host.
+ *
+ *    H->G Copy/Paste (version 4)
+ *    --------------------
+ *    When grab, GetRemoteClipboardCB got called, which sets the text,
+ *    image, rtf or file data received from the Host to the Linux Guest
+ *    OS clipboard
+ *
+ */
+
+
+#ifndef GTK4
+#error "This should build with GTK4 only."
+#endif
+
+#define G_LOG_DOMAIN "dndcp"
+
+#include <cxxabi.h>
+#include "copyPasteDnDWrapper.h"
+#include "copyPasteUIX11.h"
+#include "dndFileList.hh"
+#include "guestDnDCPMgr.hh"
+#include "tracer.hh"
+#include "vmblock.h"
+#include "fcntl.h"
+#include "file.h"
+#include "dnd.h"
+#include "dndMsg.h"
+#include "dndPluginIntX11.h"
+#include "copyPasteDnDUtil.h"
+#include <gtk/gtk.h>
+#include <sys/time.h>
+
+extern "C" {
+   #include "dndClipboard.h"
+   #include "cpName.h"
+   #include "cpNameUtil.h"
+   #include "rpcout.h"
+   #include "vmware/guestrpc/tclodefs.h"
+}
+
+static unsigned int gGuestClipGenNum;
+static unsigned int gHostClipGenNum;
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::CopyPasteUIX11 --
+ *
+ *    Constructor.
+ *
+ * Results:
+ *    None
+ *
+ * Side effects:
+ *    None
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+CopyPasteUIX11::CopyPasteUIX11()
+ : mClipboardEmpty(true),
+   mHGStagingDir(""),
+   mClipTime(0),
+   mPrimTime(0),
+   mLastTimestamp(0),
+   mThread(0),
+   mHGGetListTime(0),
+   mHGGetFileStatus(DND_FILE_TRANSFER_NOT_STARTED),
+   mBlockAdded(false),
+   mBlockCtrl(0),
+   mInited(false),
+   mDftClipboardPtr(nullptr),
+   mPrimClipboardPtr(nullptr),
+   mTotalFileSize(0)
+{
+   TRACE_CALL();
+   GuestDnDCPMgr *p = GuestDnDCPMgr::GetInstance();
+   ASSERT(p);
+   mCP = p->GetCopyPasteMgr();
+   ASSERT(mCP);
+   Glib::init();
+
+   mThreadParams.fileBlockCondExit = false;
+   pthread_mutex_init(&mThreadParams.fileBlockMutex, NULL);
+   pthread_cond_init(&mThreadParams.fileBlockCond, NULL);
+   mThreadParams.cp = this;
+   int ret = pthread_create(&mThread,
+                            NULL,
+                            FileBlockMonitorThread,
+                            (void *)&(this->mThreadParams));
+   if (ret != 0) {
+      Warning("%s: Create thread failed, errno:%d.\n", __FUNCTION__, ret);
+      mThread = 0;
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::Init --
+ *
+ *    Initialize copy paste UI class and register for V3 or greater copy
+ *    paste.
+ *
+ * Results:
+ *    Always true
+ *
+ * Side effects:
+ *    None
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+CopyPasteUIX11::Init()
+{
+   TRACE_CALL();
+   if (mInited) {
+      g_debug("%s: mInited is true\n", __FUNCTION__);
+      return true;
+   }
+
+   CPClipboard_Init(&mClipboard);
+
+   gGuestClipGenNum = 0;
+   gHostClipGenNum = 0;
+
+   mCP->srcRecvClipChanged.connect(
+      sigc::mem_fun(this, &CopyPasteUIX11::GetRemoteClipboardCB));
+   mCP->destRequestClipChanged.connect(
+      sigc::mem_fun(this, &CopyPasteUIX11::GetLocalClipboard));
+   mCP->getFilesDoneChanged.connect(
+      sigc::mem_fun(this, &CopyPasteUIX11::GetLocalFilesDone));
+
+
+   Glib::RefPtr<Gdk::Display> display = Gdk::Display::get_default();
+   mDftClipboardPtr = display->get_clipboard();
+   mPrimClipboardPtr = display->get_primary_clipboard();
+
+   mDftClipboardPtr->signal_changed().connect(
+      sigc::mem_fun(*this, &CopyPasteUIX11::GuestDefaultClipboardChangedCb));
+   mPrimClipboardPtr->signal_changed().connect(
+      sigc::mem_fun(*this, &CopyPasteUIX11::GuestPrimaryClipboardChangedCb));
+
+   mInited = true;
+   return true;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::~CopyPaste --
+ *
+ *    Destructor.
+ *
+ * Results:
+ *    None.
+ *
+ * Side effects:
+ *    None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+CopyPasteUIX11::~CopyPasteUIX11()
+{
+   TRACE_CALL();
+   CPClipboard_Destroy(&mClipboard);
+   /* Any files from last unfinished file transfer should be deleted. */
+   if (DND_FILE_TRANSFER_IN_PROGRESS == mHGGetFileStatus &&
+       !mHGStagingDir.empty()) {
+      uint64 totalSize = File_GetSizeEx(mHGStagingDir.c_str());
+      if (mTotalFileSize != totalSize) {
+         g_debug("%s: deleting %s, expecting %" FMT64 "u, finished %" FMT64 "u\n",
+                 __FUNCTION__, mHGStagingDir.c_str(),
+                 mTotalFileSize, totalSize);
+         DnD_DeleteStagingFiles(mHGStagingDir.c_str(), FALSE);
+      } else {
+         g_debug("%s: file size match %s\n",
+                 __FUNCTION__, mHGStagingDir.c_str());
+      }
+   }
+   if (mBlockAdded) {
+      g_debug("%s: removing block for %s\n", __FUNCTION__, mHGStagingDir.c_str());
+      /* We need to make sure block subsystem has not been shut off. */
+      mBlockAdded = false;
+      if (DnD_BlockIsReady(mBlockCtrl)) {
+         mBlockCtrl->RemoveBlock(mBlockCtrl->fd, mHGStagingDir.c_str());
+      }
+   }
+
+   TerminateThread();
+   pthread_mutex_destroy(&mThreadParams.fileBlockMutex);
+   pthread_cond_destroy(&mThreadParams.fileBlockCond);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::VmxCopyPasteVersionChanged --
+ *
+ *      Update version information in mCP.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::VmxCopyPasteVersionChanged(RpcChannel *chan,    // IN
+                                           uint32 version)      // IN
+{
+   ASSERT(mCP);
+   g_debug("%s: new version is %d\n", __FUNCTION__, version);
+   mCP->VmxCopyPasteVersionChanged(version);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GuestDefaultClipboardChangedCb --
+ *
+ *      Callback when the guest default clipboard changed.
+ *      Either guest own app or the H-G copy could change the guest clipboard.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      gGuestCpGenNum is increased by 1.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::GuestDefaultClipboardChangedCb()
+{
+   ++gGuestClipGenNum;
+   mClipTime = g_get_real_time();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GuestPrimaryClipboardChangedCb --
+ *
+ *      Callback when the guest primary clipboard changed.
+ *      Either guest own app or the H-G copy could change the guest clipboard.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      gGuestCpGenNum is increased by 1.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::GuestPrimaryClipboardChangedCb()
+{
+   ++gGuestClipGenNum;
+   mPrimTime = g_get_real_time();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GetLocalFilesDone --
+ *
+ *    Callback when CopyPasteUIX11::GetLocalFiles is done, which finishes the file
+ *    copying from host to guest staging directory. This function notifies
+ *    the Copy/Paste data object and end its waiting state in order to continue
+ *    the file copying from local staging directory to local target directory.
+ *
+ * Results:
+ *    None
+ *
+ * Side effects:
+ *    None
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::GetLocalFilesDone(bool success)
+{
+   g_debug("%s: enter success %d\n", __FUNCTION__, success);
+
+   if (mBlockAdded) {
+      g_debug("%s: removing block for %s\n", __FUNCTION__, mHGStagingDir.c_str());
+      /* We need to make sure block subsystem has not been shut off. */
+      mBlockAdded = false;
+      if (DnD_BlockIsReady(mBlockCtrl)) {
+         mBlockCtrl->RemoveBlock(mBlockCtrl->fd, mHGStagingDir.c_str());
+      }
+   }
+
+   mHGGetFileStatus = DND_FILE_TRANSFER_FINISHED;
+   if (success) {
+      /*
+       * Mark current staging dir to be deleted on next reboot for FCP. The
+       * file will not be deleted after reboot if it is moved to another
+       * location by target application.
+       */
+      DnD_DeleteStagingFiles(mHGStagingDir.c_str(), TRUE);
+   } else {
+      /* Copied files are already removed in common layer. */
+      mHGStagingDir.clear();
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GetLocalClipboard --
+ *
+ *    Retrieves the data from local clipboard and sends it to host. Send empty
+ *    data back if there is no data or can not get data successfully. For
+ *    guest->host copy/paste.
+ *
+ * Results:
+ *    None.
+ *
+ * Side effects:
+ *    None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::GetLocalClipboard(void)
+{
+   g_debug("%s: enter.\n", __FUNCTION__);
+   Glib::RefPtr<Gdk::Clipboard> clipboardPtr;
+
+   if (!mCP->IsCopyPasteAllowed()) {
+      g_debug("%s: copyPaste is not allowed\n", __FUNCTION__);
+      return;
+   }
+
+   if (gHostClipGenNum == gGuestClipGenNum) {
+      g_debug("%s: Guest clipboard in sync with host with the same GenNum=%d, "
+              "skip send", __FUNCTION__, gGuestClipGenNum);
+      SendClipNotChanged();
+      return;
+   }
+
+   CPClipboard_Clear(&mClipboard);
+
+   if (mClipTime > mPrimTime) {
+      if (mClipTime > 0 && mClipTime == mLastTimestamp) {
+         g_debug("%s: clip is not changed\n", __FUNCTION__);
+         SendClipNotChanged();
+         return;
+      }
+      mLastTimestamp = mClipTime;
+      clipboardPtr = mDftClipboardPtr;
+   } else {
+      if (mPrimTime > 0 && mPrimTime == mLastTimestamp) {
+         g_debug("%s: clip is not changed\n", __FUNCTION__);
+         SendClipNotChanged();
+         return;
+      }
+      mLastTimestamp = mPrimTime;
+      clipboardPtr = mPrimClipboardPtr;
+   }
+
+   Glib::RefPtr<Gdk::ContentFormats> formats = clipboardPtr->get_formats();
+
+   /*
+    * *** DEBUG ONLY log for dev to track clipboard mime formats ***
+    * TODO: Remove the below before turn FSS on
+    */
+   Glib::ustring clipFmtStr = formats->to_string();
+   const std::vector<Glib::ustring> clipFmtVector = formats->get_mime_types();
+
+   g_debug("%s: Guest clipboard formats all:'/%s/', ",  __FUNCTION__, clipFmtStr.c_str());
+   for (Glib::ustring fmt : clipFmtVector) {
+      g_debug("%s: Guest clipboard format list:'/%s/', ",  __FUNCTION__, fmt.c_str());
+
+   }
+   /* TODO: Remove the above before Turn FSS on */
+
+   /* Try to get URI's from clipboard if present. This must always be done first */
+   std::vector<Glib::ustring> fmtVec;
+   bool haveURIs = false;
+
+   if (formats->contain_mime_type(FCP_TARGET_NAME_GNOME_COPIED_FILES)) {
+      g_debug("%s: Gnome URI list is available in guest clipboard", __FUNCTION__);
+      fmtVec.push_back(FCP_TARGET_NAME_GNOME_COPIED_FILES);
+      haveURIs = true;
+   }
+   if (formats->contain_mime_type(FCP_TARGET_NAME_URI_LIST)) {
+      g_debug("%s: text uri list is available in guest clipboard", __FUNCTION__);
+      fmtVec.push_back(FCP_TARGET_NAME_URI_LIST);
+      haveURIs = true;
+   }
+   if (mCP->CheckCapability(DND_CP_CAP_FILE_CP) && haveURIs) {
+      g_debug("%s: Getting file URI from guest clipboard",  __FUNCTION__);
+      clipboardPtr->read_async(fmtVec, G_PRIORITY_DEFAULT, sigc::bind(sigc::mem_fun(
+         *this, &CopyPasteUIX11::CopyPasteRequestStreamCb), clipboardPtr), nullptr);
+      return;
+   }
+
+#if (GTK_MAJOR_VERSION == 4 && GTK_MINOR_VERSION >= 6)
+   /* Image Copy/Paste from G->H is supported from GTK 4.6 and onwards.
+    * The dependency is due to usage of gdk_texture_save_to_png_bytes().
+    * This check can be removed once we move the Host code to
+    * GTK4 as well. GDKTexture is supported from GTK4 and we can
+    * directly set it to clipboard for Image.
+    */
+
+   /* Try to get Image data from clipboard. */
+   if (mCP->CheckCapability(DND_CP_CAP_IMAGE_CP) &&
+       formats->contain_gtype(GDK_TYPE_TEXTURE)) {
+      clipboardPtr->read_texture_async(sigc::bind(sigc::mem_fun(
+         *this, &CopyPasteUIX11::CopyPasteRequestImageCb), clipboardPtr), nullptr);
+   }
+#endif
+
+   /* Try to get rtf data from clipboard if present, then check for plain text */
+   bool haveRtf = false;
+   fmtVec.clear();
+
+   if (formats->contain_mime_type(TARGET_NAME_APPLICATION_RTF)) {
+      g_debug("%s: APP RTF is available in guest clipboard", __FUNCTION__);
+      fmtVec.push_back(TARGET_NAME_APPLICATION_RTF);
+      haveRtf = true;
+   }
+   if (formats->contain_mime_type(TARGET_NAME_TEXT_RICHTEXT)) {
+      g_debug("%s: RICHTEXT is available in guest clipboard",
+              __FUNCTION__);
+      fmtVec.push_back(TARGET_NAME_TEXT_RICHTEXT);
+      haveRtf = true;
+   }
+   if (formats->contain_mime_type(TARGET_NAME_TEXT_RTF)) {
+      g_debug("%s: TEXT RTF is available in guest clipboard", __FUNCTION__);
+      fmtVec.push_back(TARGET_NAME_TEXT_RTF);
+      haveRtf = true;
+   }
+   if (mCP->CheckCapability(DND_CP_CAP_RTF_CP) && haveRtf) {
+      g_debug("%s: Getting rtf text from guest clipboard",  __FUNCTION__);
+      clipboardPtr->read_async(fmtVec, G_PRIORITY_DEFAULT, sigc::bind(sigc::mem_fun(
+        *this, &CopyPasteUIX11::CopyPasteRequestStreamCb), clipboardPtr), nullptr);
+   }
+
+   /* Try to get plain text from clipboard if present */
+   if (mCP->CheckCapability(DND_CP_CAP_PLAIN_TEXT_CP) &&
+              formats->contain_gtype(G_TYPE_STRING)) {
+      g_debug("%s: Getting plain text from guest clipboard",  __FUNCTION__);
+      clipboardPtr->read_text_async(sigc::bind(sigc::mem_fun(
+         *this, &CopyPasteUIX11::CopyPasteRequestTextCb), clipboardPtr), nullptr);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ *  CopyPasteUIX11::CopyPasteSendText--
+ *
+ *      Send the text data to Host.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *-----------------------------------------------------------------------------
+ */
+void
+CopyPasteUIX11::CopyPasteSendText(const Glib::ustring& cpStr)   // IN
+{
+   if (cpStr.empty()) {
+      return;
+   }
+
+   size_t bufSize = cpStr.size();
+   if (bufSize > 0  &&
+       bufSize <= CPCLIPITEM_MAX_SIZE_V3 &&
+       CPClipboard_SetItem(&mClipboard, CPFORMAT_TEXT,
+                           cpStr.c_str(), bufSize + 1)) {
+      g_debug("%s: set TEXT: %" FMTSZ "u\n", __FUNCTION__, bufSize);
+      mCP->DestUISendClip(&mClipboard);
+      // Reset the host and guest clip gen number.
+      gHostClipGenNum = 0;
+      gGuestClipGenNum = 0;
+   } else {
+      /* clipboard failure is not critical one,  warn but do not error exit */
+      g_warning("%s: Failed to copy text from guest clipboard,"
+                " either text is empty or too large\n", __FUNCTION__);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ *  CopyPasteSendImage--
+ *
+ *      Send the Image data to Host
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::CopyPasteSendImage(const Glib::RefPtr<Glib::Bytes>& imgData)   // IN
+{
+   if (imgData == nullptr) {
+      return;
+   }
+
+   gsize bufSize = imgData->get_size();
+   const void *buf = imgData->get_data(bufSize);
+   g_debug("%s: Got PNG byte: %" FMTSZ "u\n", __FUNCTION__, bufSize);
+
+   if (bufSize > 0  &&
+       bufSize <= CPCLIPITEM_MAX_SIZE_V3 &&
+       CPClipboard_SetItem(&mClipboard, CPFORMAT_IMG_PNG,
+                           buf, bufSize)) {
+      mCP->DestUISendClip(&mClipboard);
+      // Reset the host and guest clip gen number.
+      gHostClipGenNum = 0;
+      gGuestClipGenNum = 0;
+   } else {
+      /* clipboard failure is not critical one,  warn but do not error exit */
+      g_warning("%s: Failed to copy image from guest clipboard,"
+                " either image is empty or too large\n", __FUNCTION__);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ *  CopyPasteUIX11::CopyPasteRequestTextCb--
+ *
+ *      This Callback read's the guest's text data from clipboard. If succeeded,
+ *      the data is sent to Host.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::CopyPasteRequestTextCb(const Glib::RefPtr<Gio::AsyncResult>& result,    // IN
+                                       const Glib::RefPtr<Gdk::Clipboard>& clipboard)   // IN
+{
+   try {
+      /* gdk_clipboard_read_text_finish return NUL terminated UTF-8 string */
+      Glib::ustring cpStr = clipboard->read_text_finish(result);
+      g_debug("%s: Clip is [%s]\n", __FUNCTION__, cpStr.c_str());
+      CopyPasteSendText(cpStr);
+   } catch (const Glib::Error& e) {
+      /* clipboard failure is not critical one,  warn but do not error exit */
+      g_warning("%s: Error to copy from guest: %s\n", __FUNCTION__, e.what());
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ *  CopyPasteUIX11::CopyPasteRequestImageCb--
+ *
+ *      This Callback read's the guest's image data from clipboard. If succeeded,
+ *      the data is sent to Host.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::CopyPasteRequestImageCb(const Glib::RefPtr<Gio::AsyncResult>& result,    // IN
+                                        const Glib::RefPtr<Gdk::Clipboard>& clipboard)   // IN
+{
+   try {
+      Glib::RefPtr<Gdk::Texture> image = clipboard->read_texture_finish(result);
+      if (image) {
+         Glib::RefPtr<Glib::Bytes> imgData = image->save_to_png_bytes();
+         CopyPasteSendImage(imgData);
+      }
+   } catch (const Glib::Error& e) {
+      /* clipboard failure is not critical one,  warn but do not error exit */
+      g_warning("%s: Error to copy from guest: %s\n", __FUNCTION__, e.what());
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ *  CopyPasteSendRtfValue--
+ *
+ *      Get GBytes from the Gio::InputStream, and sent the buffer to host.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Host clipboard is changed.
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::CopyPasteSendRtfValue(const Glib::RefPtr<Gio::AsyncResult>& result)   // IN
+{
+   Glib::RefPtr<Glib::Bytes> rtfData = mCpStream->read_bytes_finish(result);
+   size_t readSize = rtfData->get_size();
+
+   g_debug("%s: Got Glib:Bytes from clipboard with size=%" FMTSZ "u\n",
+           __FUNCTION__, readSize);
+
+   if (readSize > 0) {
+      const void *buf = rtfData->get_data(readSize);
+
+      if (readSize > CPCLIPITEM_MAX_SIZE_V3) {
+         g_warning("%s: Failed to copy rtf from guest clipboard,"
+                   " as rtf data is too large\n, sizeLimit=%" FMTSZ "u, actualSize=%" FMTSZ "u",
+                   __FUNCTION__, CPCLIPITEM_MAX_SIZE_V3, readSize);
+         return;
+      }
+      if (CPClipboard_SetItem(&mClipboard, CPFORMAT_RTF,
+                              buf, readSize)) {
+         mCP->DestUISendClip(&mClipboard);
+         // Reset the host and guest clip gen number.
+         gHostClipGenNum = 0;
+         gGuestClipGenNum = 0;
+      } else {
+         /* clipboard failure is not critical one,  warn but do not error exit */
+         g_warning("%s: Failed to set rtf data to dest buffer from guest clipboard", __FUNCTION__);
+      }
+   } else {
+      g_warning("%s: Failed to copy rtf from guest clipboard,"
+               " as rtf data is empty\n", __FUNCTION__);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::CopyPasteSendFile --
+ *
+ *      Construct local file list and remote file list and send it to Host.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::CopyPasteSendFile(const Glib::RefPtr<Gio::AsyncResult>& result)      // IN
+{
+   char *newPath;
+   size_t newPathLen;
+   size_t index = 0;
+   DnDFileList fileList;
+   DynBuf buf;
+   uint64 totalSize = 0;
+   int64 size;
+
+   /*
+    * Turn the uri list into two \0  delimited lists. One for full paths and
+    * one for just the last path component.
+    */
+   Glib::RefPtr<Glib::Bytes> fileUriData = mCpStream->read_bytes_finish(result);
+   gsize readSize = fileUriData->get_size();
+
+   g_debug("%s: Got Glib:Bytes from clipboard with size=%" FMTSZ "u\n",
+           __FUNCTION__, readSize);
+
+   if (readSize <= 0) {
+      g_warning("%s: Failed to copy file from guest clipboard,"
+                " as file uri data is empty or not valid.\n", __FUNCTION__);
+      return;
+   }
+
+   const char *data = static_cast<const char*>(fileUriData->get_data(readSize));
+   /* Converting data to utf::string directly is leading to buffer overflow.
+    * Its because data is Byte data and not NULL terminated. utf::string
+    * has no implementation for string creation with data size.
+    * In order to avoid that, first converting it to std::string and then
+    * converting to utf::string.
+    */
+   std::string str(data, readSize);
+   utf::string source(str.c_str());
+   g_debug("%s: Got file list: [%s]\n", __FUNCTION__, source.c_str());
+
+   /*
+    * In gnome, before file list there may be a extra line indicating it
+    * is a copy or cut.
+    */
+   if (source.startsWith("copy\n")) {
+      source = source.erase(0, 5);
+   }
+
+   if (source.startsWith("cut\n")) {
+      source = source.erase(0, 4);
+   }
+
+   while (source.bytes() > 0 &&
+          (source[0] == '\n' || source[0] == '\r' || source[0] == ' ')) {
+      source = source.erase(0, 1);
+   }
+
+   while ((newPath = DnD_UriListGetNextFile(source.c_str(),
+                                            &index,
+                                            &newPathLen)) != NULL) {
+      char *newRelPath;
+
+#if defined(__linux__)
+      if (DnD_UriIsNonFileSchemes(newPath)) {
+         /* Try to get local file path for non file uri. */
+         GFile *file = g_file_new_for_uri(newPath);
+         free(newPath);
+         if (!file) {
+            g_debug("%s: g_file_new_for_uri failed\n", __FUNCTION__);
+            return;
+         }
+         newPath = g_file_get_path(file);
+         g_object_unref(file);
+         if (!newPath) {
+            g_debug("%s: g_file_get_path failed\n", __FUNCTION__);
+            return;
+         }
+      }
+#endif
+
+      /*
+       * Parse relative path.
+       */
+      newRelPath = Str_Strrchr(newPath, DIRSEPC) + 1; // Point to char after '/'
+
+      /* Keep track of how big the fcp files are. */
+      if ((size = File_GetSizeEx(newPath)) >= 0) {
+         totalSize += size;
+      } else {
+         g_debug("%s: Unable to get file size for %s\n", __FUNCTION__, newPath);
+      }
+
+      g_debug("%s: Adding newPath '%s' newRelPath '%s'\n", __FUNCTION__,
+            newPath, newRelPath);
+      fileList.AddFile(newPath, newRelPath);
+      free(newPath);
+   }
+
+   DynBuf_Init(&buf);
+   bool validDataInClip = false;
+   fileList.SetFileSize(totalSize);
+   g_debug("%s: totalSize is %" FMT64 "u\n", __FUNCTION__, totalSize);
+   fileList.ToCPClipboard(&buf, false);
+   gsize bufSize = DynBuf_GetSize(&buf);
+   if (bufSize > 0  &&
+       bufSize <= (int)CPCLIPITEM_MAX_SIZE_V3 &&
+       CPClipboard_SetItem(&mClipboard, CPFORMAT_FILELIST, DynBuf_Get(&buf),
+                           bufSize)) {
+      validDataInClip = true;
+      g_debug("%s: Got File List. \n", __FUNCTION__);
+      // Reset the host and guest clip gen number.
+      gHostClipGenNum = 0;
+      gGuestClipGenNum = 0;
+   } else {
+      g_debug("%s: Failed to get File List\n", __FUNCTION__);
+   }
+
+   DynBuf_Destroy(&buf);
+
+   if (validDataInClip) {
+      /*
+       * File List in the clipboard.
+       */
+      mCP->DestUISendClip(&mClipboard);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ *  CopyPasteRequestStreamCb--
+ *
+ *      Get Gio::InputStream from clipboard, need to read from the resulting
+ *      InputStream Async.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      class member mCpStream is modified.
+ *-----------------------------------------------------------------------------
+ */
+
+ void
+ CopyPasteUIX11::CopyPasteRequestStreamCb(const Glib::RefPtr<Gio::AsyncResult>& result,   // IN
+                                          const Glib::RefPtr<Gdk::Clipboard>& clipboard)  // IN
+ {
+    Glib::ustring out_mime_type;
+    /* Get the InputStream from clipboard */
+    mCpStream = clipboard->read_finish(result, out_mime_type);
+    g_debug("%s: Got rtf format [%s] from clipboard\n", __FUNCTION__, out_mime_type.c_str());
+    /*
+     * get GBytes from the stream and CopyPasteSendValue callback when finish.
+     * Here, a max of CPCLIPITEM_MAX_SIZE_V3+1 size is set, if we really read that much
+     * then we could error out after read_bytes_finish is called.
+     */
+    if (out_mime_type == TARGET_NAME_APPLICATION_RTF ||
+        out_mime_type == TARGET_NAME_TEXT_RICHTEXT ||
+       out_mime_type == TARGET_NAME_TEXT_RTF) {
+        mCpStream->read_bytes_async(CPCLIPITEM_MAX_SIZE_V3 + 1,
+           sigc::mem_fun(*this, &CopyPasteUIX11::CopyPasteSendRtfValue), G_PRIORITY_DEFAULT);
+    } else if (out_mime_type == FCP_TARGET_NAME_GNOME_COPIED_FILES ||
+              out_mime_type == FCP_TARGET_NAME_URI_LIST) {
+        mCpStream->read_bytes_async(CPCLIPITEM_MAX_SIZE_V3 + 1,
+           sigc::mem_fun(*this, &CopyPasteUIX11::CopyPasteSendFile), G_PRIORITY_DEFAULT);
+    }
+ }
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::LocalSetFileListToClipboard --
+ *
+ *      API to set the guest clipboard with file list.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      gHostClipGenNum is increased.
+ *      Clipboard is set with File list.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::LocalSetFileListToClipboard(const char* desktop)   // IN
+{
+   std::vector<Glib::RefPtr<Gdk::ContentProvider>> provider;
+   Glib::RefPtr<Gdk::ContentProvider> providerUnion;
+   Glib::RefPtr<Glib::Bytes> fileListGnome;
+   Glib::RefPtr<Glib::Bytes> fileListKde;
+   Glib::RefPtr<Glib::Bytes> fileListNautilus;
+
+   g_debug("%s: enter.\n", __FUNCTION__);
+   g_debug("%s: Desktop = %s\n", __FUNCTION__, desktop);
+
+   if (desktop && strstr(desktop, "GNOME")) {
+      // Set the Gnome file list to GDKContentProvider
+      fileListGnome = Glib::Bytes::create(mHGCopiedUriListGnome.c_str(), mHGCopiedUriListGnome.bytes());
+      fileListNautilus = Glib::Bytes::create(mHGCopiedUriListNautilus.c_str(), mHGCopiedUriListNautilus.bytes());
+      provider.push_back(Gdk::ContentProvider::create(FCP_TARGET_NAME_GNOME_COPIED_FILES, fileListGnome));
+      provider.push_back(Gdk::ContentProvider::create(FCP_TARGET_NAME_NAUTILUS_GTK4_UTF8, fileListNautilus));
+   } else if (desktop && strstr(desktop, "KDE")) {
+      // Set the Kde file list to GDKContentProvider
+      fileListKde = Glib::Bytes::create(mHGCopiedUriListKde.c_str(), mHGCopiedUriListKde.bytes());
+      provider.push_back(Gdk::ContentProvider::create(FCP_TARGET_NAME_URI_LIST, fileListKde));
+   } else {
+      g_warning("%s: Unsupported Desktop Manager %s. \n", __FUNCTION__, desktop);
+      return;
+   }
+
+   providerUnion = Gdk::ContentProvider::create(provider);
+   mDftClipboardPtr->set_content(providerUnion);
+   mPrimClipboardPtr->set_content(providerUnion);
+
+   /* Increase the Host Clip number by 2 as we are updating the
+    * Clipboard content for both default and Primary Clipboard.
+    */
+   gHostClipGenNum += 2;
+   return;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::BuildFileURIList --
+ *
+ *      Used to populate the file URI lists for GNOME, Nautilus, and KDE.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::BuildFileURIList(utf::string& uriList,         // IN/OUT
+                                utf::string pre,              // IN
+                                 utf::string post,             // IN
+                                utf::string stagingDirName)   // IN
+{
+   size_t pos  = 0;
+   utf::string str;
+
+   while ((str = GetNextPath(mHGFCPData, pos).c_str()).bytes() != 0) {
+      g_debug("%s: Path: %s", __FUNCTION__, str.c_str());
+      uriList += pre;
+      if (mBlockAdded) {
+         uriList += mBlockCtrl->blockRoot;
+         uriList += DIRSEPS + stagingDirName + DIRSEPS + str + post;
+      } else {
+         uriList += DIRSEPS + mHGStagingDir + DIRSEPS + str + post;
+      }
+   }
+}
+
+
+/*    
+ *-----------------------------------------------------------------------------
+ *    
+ * CopyPasteUIX11::BuildFileContentURIList --
+ *    
+ *      Used to populate the file content URI lists for GNOME, Nautilus, and KDE.
+ *    
+ * Results:
+ *      None.
+ *    
+ * Side effects:
+ *      None.
+ *    
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::BuildFileContentURIList(utf::string& uriList,         // IN/OUT
+                                        utf::string pre,              // IN
+                                        utf::string post)             // IN
+{
+   for (const auto& path : mHGFileContentsList) {
+      uriList += pre + path + post;
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::LocalGetFileRequest --
+ *
+ *      Begins copying the files from host to guest and return the file list.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::LocalGetFileRequest()
+{
+   g_debug("%s: enter.\n", __FUNCTION__);
+
+   if (!mCP->IsCopyPasteAllowed()) {
+      g_debug("%s: copy paste not allowed, returning.\n",
+            __FUNCTION__);
+      return;
+   }
+
+   const char *desktop = getenv("XDG_CURRENT_DESKTOP");
+
+   if (mHGGetFileStatus != DND_FILE_TRANSFER_NOT_STARTED) {
+      /*
+       * On KDE (at least), we can see this multiple times, so ignore if
+       * we are already getting files.
+       */
+      g_debug("%s: GetFiles already started, returning uriList [%s]\n",
+              __FUNCTION__, mHGCopiedUriListGnome.c_str());
+      LocalSetFileListToClipboard(desktop);
+      return;
+   } else {
+      utf::string hgStagingDir;
+      utf::string stagingDirName;
+
+      hgStagingDir = utf::CopyAndFree(DnD_CreateStagingDirectory());
+      g_debug("%s: Getting files. Staging dir: %s", __FUNCTION__,
+            hgStagingDir.c_str());
+
+      if (0 == hgStagingDir.bytes()) {
+         g_debug("%s: Can not create staging directory\n", __FUNCTION__);
+        mDftClipboardPtr->unset_content();
+        mPrimClipboardPtr->unset_content();
+         return;
+      }
+      mHGGetFileStatus = DND_FILE_TRANSFER_IN_PROGRESS;
+
+      mBlockAdded = false;
+      if (DnD_BlockIsReady(mBlockCtrl) && mBlockCtrl->AddBlock(mBlockCtrl->fd, hgStagingDir.c_str())) {
+         g_debug("%s: add block for %s.\n",
+               __FUNCTION__, hgStagingDir.c_str());
+         mBlockAdded = true;
+         pthread_mutex_lock(&mThreadParams.fileBlockMutex);
+         mThreadParams.fileBlockCondExit = false;
+         mThreadParams.fileBlockName = VMBLOCK_FUSE_NOTIFY_ROOT;
+         mThreadParams.fileBlockName += DIRSEPS;
+         mThreadParams.fileBlockName += GetLastDirName(hgStagingDir);
+         pthread_cond_signal(&mThreadParams.fileBlockCond);
+         pthread_mutex_unlock(&mThreadParams.fileBlockMutex);
+      } else {
+         g_debug("%s: unable to add block for %s.\n",
+               __FUNCTION__, hgStagingDir.c_str());
+      }
+
+      mHGStagingDir = hgStagingDir;
+
+      /* Provide path within vmblock file system instead of actual path. */
+      stagingDirName = GetLastDirName(hgStagingDir);
+      if (stagingDirName.empty()) {
+         g_debug("%s: Can not get staging directory name\n", __FUNCTION__);
+        mDftClipboardPtr->unset_content();
+        mPrimClipboardPtr->unset_content();
+         return;
+      }
+
+      if (desktop && strstr(desktop, "GNOME")) {
+         /* Setting Gnome URIs for each path in the guest's file list. */
+         mHGCopiedUriListGnome = "copy\n";
+        BuildFileURIList(mHGCopiedUriListGnome, FCP_GNOME_LIST_PRE, FCP_GNOME_LIST_POST, stagingDirName);
+
+         /* Nautilus on Gnome does not expect FCP_GNOME_LIST_POST after the last uri. See bug 143147. */
+        mHGCopiedUriListGnome.erase(mHGCopiedUriListGnome.size() - strlen(FCP_GNOME_LIST_POST));
+        g_debug("%s: providing file list [%s]\n", __FUNCTION__,
+                 mHGCopiedUriListGnome.c_str());
+
+        /* Setting Nautilus URIs for each path in the guest's file list. */
+         mHGCopiedUriListNautilus = utf::string(FCP_TARGET_MIME_NAUTILUS_FILES) + "\ncopy\n";
+        BuildFileURIList(mHGCopiedUriListNautilus, FCP_GNOME_LIST_PRE, FCP_GNOME_LIST_POST, stagingDirName);
+      } else if (desktop && strstr(desktop, "KDE")) {
+         /* Setting KDE URIs for each path in the guest's file list. */
+        BuildFileURIList(mHGCopiedUriListKde, DND_URI_LIST_PRE_KDE, DND_URI_LIST_POST, stagingDirName);
+        g_debug("%s: providing file list [%s]\n", __FUNCTION__,
+                 mHGCopiedUriListKde.c_str());
+      } else {
+        g_warning("%s: Unsupported Desktop Manager %s. \n", __FUNCTION__, desktop);
+         return;
+      }
+   }
+
+   if (!mBlockAdded) {
+      /*
+       * If there is no blocking driver, wait here till file copy is done.
+       * 2 reasons to keep this:
+       * 1. If run vmware-user stand-alone as non-root, blocking driver can
+       *    not be opened. g_debug purpose only.
+       * 2. Other platforms (Solaris, etc) may also use this code,
+       *    and there is no blocking driver yet.
+       *
+       * Polling here will not be sufficient for large files (experiments
+       * showed it was sufficient for a 256MB file, and failed for a 1GB
+       * file, but those numbers are of course context-sensitive and so YMMV).
+       * The reason is we are executing in the context of gtkmm callback, and
+       * apparently it only has so much patience regarding how quickly we
+       * return.
+       */
+      CopyPasteDnDWrapper *wrapper = CopyPasteDnDWrapper::GetInstance();
+      ToolsAppCtx *ctx = wrapper->GetToolsAppCtx();
+      while (mHGGetFileStatus == DND_FILE_TRANSFER_IN_PROGRESS) {
+         struct timeval tv {};
+
+         tv.tv_sec = 0;
+         g_main_context_iteration(g_main_loop_get_context(ctx->mainLoop),
+                                  FALSE);
+         if (select(0, NULL, NULL, NULL, &tv) == -1) {
+            g_debug("%s: error in select (%s).\n", __FUNCTION__,
+                    strerror(errno));
+           mDftClipboardPtr->unset_content();
+           mPrimClipboardPtr->unset_content();
+            return;
+         }
+      }
+      g_debug("%s: file transfer done!\n", __FUNCTION__);
+   }
+
+   /* Set the content of clipboard with the File URI's. */
+   LocalSetFileListToClipboard(desktop);
+   return;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GetLastDirName --
+ *
+ *      Try to get last directory name from a full path name.
+ *
+ * Results:
+ *      Last dir name in the full path name if success, empty str otherwise
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+utf::string
+CopyPasteUIX11::GetLastDirName(const utf::string &str)     // IN
+{
+   /* This is a common function with DNDUIX11 class.
+    * When we implement Drag and Drop with GTK4, we can
+    * evaluate and see if this needs to be moved to a common
+    * util file.
+    */
+
+   char *baseName;
+   File_GetPathName(str.c_str(), NULL, &baseName);
+   return utf::CopyAndFree(baseName);
+}
+
+
+/*
+ *----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GetNextPath --
+ *
+ *      Provides a substring containing the next path from the provided
+ *      NUL-delimited string starting at the provided index.
+ *
+ * Results:
+ *      A string with the next path or "" if there are no more paths.
+ *
+ * Side effects:
+ *      None.
+ *
+ *----------------------------------------------------------------------------
+ */
+
+utf::utf8string
+CopyPasteUIX11::GetNextPath(utf::utf8string& str, // IN: NUL-delimited path list
+                            size_t& index)        // IN/OUT: current index into string
+{
+   /* This is a common function with DNDUIX11 class.
+    * When we implement Drag and Drop with GTK4, we can
+    * evaluate and see if this needs to be moved to a common
+    * util file.
+    */
+
+   utf::utf8string ret;
+   size_t start;
+
+   if (index >= str.length()) {
+      return "";
+   }
+
+   for (start = index; str[index] != '\0' && index < str.length(); index++) {
+      /*
+       * Escape reserved characters according to RFC 1630.  We'd use
+       * Escape_Do() if this wasn't a utf::string, but let's use the same table
+       * replacement approach.
+       */
+      static char const Dec2Hex[] = {
+         '0', '1', '2', '3', '4', '5', '6', '7',
+         '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
+      };
+
+      unsigned char ubyte = str[index];
+
+      if (ubyte == '#' ||   /* Fragment identifier delimiter */
+          ubyte == '?' ||   /* Query string delimiter */
+          ubyte == '*' ||   /* "Special significance within specific schemes" */
+          ubyte == '!' ||   /* "Special significance within specific schemes" */
+          ubyte == '%' ||   /* Escape character */
+          ubyte >= 0x80) {  /* UTF-8 encoding bytes */
+         str.replace(index, 1, "%");
+         str.insert(index + 1, 1, Dec2Hex[ubyte >> 4]);
+         str.insert(index + 2, 1, Dec2Hex[ubyte & 0xF]);
+         index += 2;
+      }
+   }
+
+   ret = str.substr(start, index - start);
+   g_debug("%s: nextpath: %s", __FUNCTION__, ret.c_str());
+   index++;
+   return ret;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GetCurrentTime --
+ *
+ *    Get current time in microseconds.
+ *
+ * Results:
+ *    Time in microseconds.
+ *
+ * Side effects:
+ *    None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+VmTimeType
+CopyPasteUIX11::GetCurrentTime(void)
+{
+   struct timeval tv;
+   VmTimeType curTime;
+
+   if (gettimeofday(&tv, NULL) != 0) {
+      g_debug("%s: gettimeofday failed!\n", __FUNCTION__);
+      return static_cast<VmTimeType>(0);
+   }
+   curTime = (tv.tv_sec * 1000000 + tv.tv_usec);
+   return curTime;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::LocalGetFileContentsRequest --
+ *
+ *      Sets the files content uri list to clipboard.
+ *
+ *      H->G only.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::LocalGetFileContentsRequest()
+{
+   if (!mCP->CheckCapability(DND_CP_CAP_FILE_CONTENT_CP)) {
+      /*
+       * Disallowed based on caps settings, return.
+       */
+      return;
+   }
+
+   const char *desktop = getenv("XDG_CURRENT_DESKTOP");
+
+   if (desktop && strstr(desktop, "GNOME")) {
+      /* Setting Gnome URIs for each path in the guest's file list. */
+      mHGCopiedUriListGnome = "copy\n";
+      BuildFileContentURIList(mHGCopiedUriListGnome, FCP_GNOME_LIST_PRE, FCP_GNOME_LIST_POST);
+
+      /* Nautilus on Gnome does not expect FCP_GNOME_LIST_POST after the last uri. See bug 143147. */
+      mHGCopiedUriListGnome.erase(mHGCopiedUriListGnome.size() - strlen(FCP_GNOME_LIST_POST));
+      g_debug("%s: providing file list [%s]\n", __FUNCTION__, mHGCopiedUriListGnome.c_str());
+
+      /* Setting Nautilus URIs for each path in the guest's file list. */
+      mHGCopiedUriListNautilus = utf::string(FCP_TARGET_MIME_NAUTILUS_FILES) + "\ncopy\n";
+      BuildFileContentURIList(mHGCopiedUriListNautilus, FCP_GNOME_LIST_PRE, FCP_GNOME_LIST_POST);
+   } else if (desktop && strstr(desktop, "KDE")) {
+      /* Setting KDE URIs for each path in the guest's file list. */
+      BuildFileContentURIList(mHGCopiedUriListKde, DND_URI_LIST_PRE_KDE, DND_URI_LIST_POST);
+      g_debug("%s: providing file list [%s]\n", __FUNCTION__, mHGCopiedUriListKde.c_str());
+   } else {
+      g_warning("%s: Unsupported Desktop Manager %s. \n", __FUNCTION__, desktop);
+      return;
+   }
+
+   /* Set the content of clipboard with the File URI's. */
+   LocalSetFileListToClipboard(desktop);
+   return;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::GetRemoteClipboardCB --
+ *
+ *    Invoked when got data from host. Update the internal data to get the file
+ *    names or the text that needs to be transferred.
+ *
+ *    Method for copy and paste from host to guest.
+ *
+ * Results:
+ *    None
+ *
+ * Side effects:
+ *    None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::GetRemoteClipboardCB(const CPClipboard *clip) // IN
+{
+   g_debug("%s: enter.", __FUNCTION__);
+   void *buf;
+   size_t sz;
+   std::vector<Glib::RefPtr<Gdk::ContentProvider>> provider;
+
+
+   TRACE_CALL();
+   if (!clip) {
+      g_debug("%s: No clipboard contents.", __FUNCTION__);
+      return;
+   }
+
+   if (mBlockAdded) {
+      mBlockAdded = false;
+      if (DnD_BlockIsReady(mBlockCtrl)) {
+         mBlockCtrl->RemoveBlock(mBlockCtrl->fd, mHGStagingDir.c_str());
+      }
+   }
+
+   mHGFCPData.clear();
+
+   if (CPClipboard_ItemExists(clip, CPFORMAT_TEXT) ||
+       CPClipboard_ItemExists(clip, CPFORMAT_RTF)) {
+      Glib::RefPtr<Gdk::ContentProvider> providerUnion;
+      Glib::RefPtr<Glib::Bytes> textData;
+      Glib::RefPtr<Glib::Bytes> rtfData;
+
+      if (CPClipboard_GetItem(clip, CPFORMAT_RTF, &buf, &sz)) {
+         g_debug("%s: RTF data, size %" FMTSZ "u.\n", __FUNCTION__, sz);
+        rtfData = Glib::Bytes::create(buf, sz);
+        provider.push_back(Gdk::ContentProvider::create(TARGET_NAME_APPLICATION_RTF, rtfData));
+        provider.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_RICHTEXT, rtfData));
+        provider.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_RTF, rtfData));
+      }
+
+      if (CPClipboard_GetItem(clip, CPFORMAT_TEXT, &buf, &sz)) {
+         g_debug("%s: Text data, size %" FMTSZ "u.\n", __FUNCTION__, sz);
+        textData = Glib::Bytes::create(buf, sz);
+        provider.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_PLAIN, textData));
+        provider.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_PLAIN_UTF8, textData));
+      }
+
+      providerUnion = Gdk::ContentProvider::create(provider);
+
+      if (providerUnion) {
+         mDftClipboardPtr->set_content(providerUnion);
+        mPrimClipboardPtr->set_content(providerUnion);
+         g_debug("%s: RTF/Text data is set to Clipboard. \n", __FUNCTION__);
+
+        /* Increase the Host Clip number by 2 as we are updating the
+         * Clipboard content for both default and Primary Clipboard.
+         */
+        gHostClipGenNum += 2;
+      } else {
+         g_debug("%s: Error in setting RTF/Text data to clipboard. \n", __FUNCTION__);
+      }
+
+      return;
+   }
+
+   if (CPClipboard_GetItem(clip, CPFORMAT_IMG_PNG, &buf, &sz)) {
+      g_debug("%s: PNG data, size %" FMTSZ "u.\n", __FUNCTION__, sz);
+      /* Try to load buf into gdk texture, and write to local clipboard. */
+      Glib::RefPtr<Glib::Bytes> imgData = Glib::Bytes::create(buf, sz);
+
+      try {
+         Glib::RefPtr<Gdk::Texture> image = Gdk::Texture::create_from_bytes(imgData);
+         if (image) {
+           mDftClipboardPtr->set_texture(image);
+           mPrimClipboardPtr->set_texture(image);
+            g_debug("%s: Image data is set to Clipboard. \n", __FUNCTION__);
+
+           /* Increase the Host Clip number by 2 as we are updating the
+             * Clipboard content for both default and Primary Clipboard.
+             */
+            gHostClipGenNum += 2;
+        }
+      } catch (const Glib::Error& e) {
+         /* clipboard failure is not critical one,  warn but do not error exit */
+         g_warning("%s: Error in setting Image data to clipboard: %s\n", __FUNCTION__, e.what());
+      }
+
+      return;
+   }
+
+   if (CPClipboard_GetItem(clip, CPFORMAT_FILELIST, &buf, &sz)) {
+      g_debug("%s: File data.\n", __FUNCTION__);
+      DnDFileList flist;
+      flist.FromCPClipboard(buf, sz);
+      mTotalFileSize = flist.GetFileSize();
+      mHGFCPData = flist.GetRelPathsStr();
+      mHGGetListTime = GetCurrentTime();
+      mHGGetFileStatus = DND_FILE_TRANSFER_NOT_STARTED;
+      mHGCopiedUriListGnome.clear();
+      mHGCopiedUriListKde.clear();
+      mHGCopiedUriListNautilus.clear();
+      LocalGetFileRequest();
+   }
+
+   if (CPClipboard_ItemExists(clip, CPFORMAT_FILECONTENTS)) {
+      g_debug("%s: File contents data\n", __FUNCTION__);
+      if (LocalPrepareFileContents(clip, mHGFileContentsList)) {
+         mHGCopiedUriListGnome.clear();
+         mHGCopiedUriListKde.clear();
+         mHGCopiedUriListNautilus.clear();
+         LocalGetFileContentsRequest();
+      }
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::SendClipNotChanged --
+ *
+ *    Send a not-changed clip to host.
+ *
+ * Results:
+ *    None.
+ *
+ * Side effects:
+ *    None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::SendClipNotChanged(void)
+{
+   CPClipboard clip;
+
+   g_debug("%s: enter.\n", __FUNCTION__);
+   CPClipboard_Init(&clip);
+   CPClipboard_SetChanged(&clip, FALSE);
+   mCP->DestUISendClip(&clip);
+   CPClipboard_Destroy(&clip);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::Reset --
+ *
+ *
+ * Results:
+ *    None
+ *
+ * Side effects:
+ *    None
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::Reset(void)
+{
+   TRACE_CALL();
+   /* Cancel any pending file transfer. */
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::FileBlockMonitorThread --
+ *
+ *    This thread monitors the access to the files in the clipboard which owns
+ *    by VMTools by using the notification mechanism of VMBlock.
+ *    If any access to the file is detected, then this thread requests the file
+ *    transfer from host to guest.
+ *
+ * Results:
+ *    Always return NULL
+ *
+ * Side effects:
+ *    None
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void*
+CopyPasteUIX11::FileBlockMonitorThread(void *arg)   // IN
+{
+   /* This is a common function with GTK3 implementation of CopyPasteUIX11 class.
+    * Keeping it seperate as this API modifies the class member variables
+    * mThread and mThreadParams.
+    */
+
+   TRACE_CALL();
+   ThreadParams *params = static_cast<ThreadParams*>(arg);
+   pthread_mutex_lock(&params->fileBlockMutex);
+   while (true) {
+      g_debug("%s: waiting signal\n", __FUNCTION__);
+      pthread_cond_wait(&params->fileBlockCond, &params->fileBlockMutex);
+      g_debug("%s: received signal. Exit:%d\n",
+              __FUNCTION__,
+              params->fileBlockCondExit);
+      if (params->fileBlockCondExit) {
+         break;
+      }
+      if (params->fileBlockName.bytes() == 0) {
+        continue;
+      }
+
+      int fd = open(params->fileBlockName.c_str(), O_RDONLY);
+      if (fd < 0) {
+         g_debug("%s: Failed to open %s, errno is %d\n",
+                 __FUNCTION__,
+                 params->fileBlockName.c_str(),
+                 errno);
+         continue;
+      }
+
+      char buf[sizeof VMBLOCK_FUSE_READ_RESPONSE];
+      ssize_t size;
+      size = read(fd, buf, sizeof buf);
+      g_debug("%s: Number of bytes read : %" FMTSZ "d\n", __FUNCTION__, size);
+      /*
+       * The current thread will block in read function until
+       * any other application accesses the file params->fileBlockName
+       * or the block on the file params->fileBlockName is removed.
+       * Currently we don't need to check the response in buf, so
+       * just ignore it.
+       */
+
+      if (params->cp->IsBlockAdded()) {
+         g_debug("%s: Request files\n", __FUNCTION__);
+         params->cp->RequestFiles();
+      } else {
+         g_debug("%s: Block is not added\n", __FUNCTION__);
+      }
+
+      if (close(fd) < 0) {
+         g_debug("%s: Failed to close %s, errno is %d\n",
+                 __FUNCTION__,
+                 params->fileBlockName.c_str(),
+                 errno);
+      }
+   }
+   pthread_mutex_unlock(&params->fileBlockMutex);
+   return NULL;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * CopyPasteUIX11::TerminateThread --
+ *
+ *    This is called when the monitor thread is asked to exit.
+ *    Sets the global state and signals the monitor thread to exit and wait
+ *    for the monitor thread to exit.
+ *
+ * Results:
+ *    None
+ *
+ * Side effects:
+ *    None
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+CopyPasteUIX11::TerminateThread()
+{
+   /* This is a common function with GTK3 implementation of CopyPasteUIX11 class.
+    * Keeping it seperate as this API modifies the class member variables
+    * mThread and mThreadParams.
+    */
+
+   TRACE_CALL();
+   if (mThread == 0) {
+      return;
+   }
+
+   pthread_mutex_lock(&mThreadParams.fileBlockMutex);
+   mThreadParams.fileBlockCondExit = true;
+   pthread_cond_signal(&mThreadParams.fileBlockCond);
+   pthread_mutex_unlock(&mThreadParams.fileBlockMutex);
+
+   pthread_join(mThread, NULL);
+
+   mThread = 0;
+}
+
index f15c6b2287929d0e0571cb22dc1d85be8b1c7e89..82ec3fa618246143ee137005a9beae7f2acaf8ff 100644 (file)
@@ -106,7 +106,11 @@ extern "C" {
 #define DRAG_LEAVE_TIMEOUT         500
 
 /* Guest detection window width and height. */
+#ifdef GTK4
+#define DRAG_DET_WINDOW_WIDTH 62
+#else
 #define DRAG_DET_WINDOW_WIDTH 31
+#endif
 
 /* Clipboard image size limit. */
 #define CLIPBOARD_IMAGE_MAX_WIDTH  4000
index 9acc3cc6a59c2a99de0d646f0cf3986ad7b9bbf6..ed3b2c5e577ce07a664ed31b459377e71d20e38a 100644 (file)
 
 #define VMTOOLS_USE_LEGACY_GTK
 
+#ifdef GTK4
+#undef VMTOOLS_USE_LEGACY_GTK
+#endif
 
 #include "dnd.h"     /* for DnDBlockControl */
+#ifdef GTK4
+#include <gtkmm/application.h>
+#include "dndUIX11GTK4.h"
+
+#endif
 
 #ifdef VMTOOLS_USE_LEGACY_GTK
 #include "dndUIX11.h"
diff --git a/open-vm-tools/services/plugins/dndcp/dndGuestBase/dndUIX11GTK4.h b/open-vm-tools/services/plugins/dndcp/dndGuestBase/dndUIX11GTK4.h
new file mode 100644 (file)
index 0000000..f18c7e2
--- /dev/null
@@ -0,0 +1,216 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+/**
+ * @file dndUIX11GTK4.h
+ *
+ *    Implement the methods that allow DnD between host and guest for
+ *    protocols V3 or greater on GTK4 lib.
+ *
+ */
+
+#ifndef __DND_UI_X11_GTK4_H__
+#define __DND_UI_X11_GTK4_H__
+
+#include "stringxx/string.hh"
+#include "dnd.h"
+#include "str.h"
+#include "util.h"
+#include "vmblock.h"
+#include "dynbuf.h"
+#include "dynxdr.h"
+#include "posix.h"
+
+extern "C" {
+#include "debug.h"
+#include "dndClipboard.h"
+#include "../dnd/dndFileContentsUtil.h"
+#include "cpNameUtil.h"
+#include "vmware/tools/guestrpc.h"
+#include "vmware/tools/plugin.h"
+}
+
+#include "guestDnD.hh"
+#include "dndFileList.hh"
+#include "dragDetWndX11GTK4.h"
+
+struct DblLnkLst_Links;
+
+/**
+ * The DnDUI class implements the UI portion of DnD V3 and greater
+ * versions of the protocol.
+ */
+class DnDUIX11
+   : public sigc::trackable
+{
+public:
+   DnDUIX11(ToolsAppCtx *ctx);
+   ~DnDUIX11();
+   bool Init();
+   void VmxDnDVersionChanged(RpcChannel *chan, uint32 version);
+   void SetDnDAllowed(bool isDnDAllowed)
+      {ASSERT(mDnD); mDnD->SetDnDAllowed(isDnDAllowed);}
+   void SetBlockControl(DnDBlockControl *blockCtrl);
+
+private:
+   /*
+    * Blocking FS Helper Functions.
+    */
+   void AddBlock();
+   void RemoveBlock();
+   void
+   BuildFileURIList(utf::string& uriList,
+                    utf::string pre,
+                    utf::string post,
+                    utf::string stagingDirName);
+   void
+   BuildFileContentURIList(utf::string& uriList,
+                           utf::string pre,
+                           utf::string post);
+   void LocalGetFileRequest(utf::string stagingDirName, const char *desktop);
+   void LocalGetFileContentsRequest(const char *desktop);
+   void LocalSetContentProvider(const char* desktop);
+
+   /*
+    * Callbacks from Common DnD layer.
+    */
+
+   void ResetUI();
+   void OnMoveMouse(int32 x, int32 y);
+
+   /*
+    * Source functions for HG DnD.
+    */
+   void OnSrcDragBegin(const CPClipboard *clip, std::string stagingDir);
+   void OnSrcDrop();
+   void OnSrcCancel();
+
+   /*
+    * Called when GH DnD is completed.
+    */
+   void OnPrivateDrop(int32 x, int32 y);
+   void OnDestCancel();
+
+   /*
+    * Source functions for file transfer.
+    */
+   void OnGetFilesDone(bool success);
+
+   /*
+    * Callbacks for showing/hiding detection window.
+    */
+   void OnUpdateDetWnd(bool bShow, int32 x, int32 y);
+   /* TODO: Unity related functions are deprecated, remove it later */
+   void OnUpdateUnityDetWnd(bool bShow, uint32 unityWndId, bool bottom);
+   void OnDestMoveDetWndToMousePos();
+   void SourceDragStartDone();
+   void SourceUpdateFeedback(DND_DROPEFFECT effect);
+
+   /*
+    * Gtk+ Callbacks: Drag Source
+    */
+   static void OnGtkSrcDragBegin(const Glib::RefPtr< Gdk::Drag > &drag, void *ctx);
+   static void OnGtkSrcDragEnd(const Glib::RefPtr< Gdk::Drag > &drag, bool delete_data, void *ctx);
+   static bool OnGtkSrcDragCancel(const Glib::RefPtr< Gdk::Drag > &drag, Gdk::DragCancelReason reason, void *ctx);
+
+   /*
+    * Gtk+ Callbacks: Drag Destination.
+    */
+   static bool OnGtkDragAccept(const Glib::RefPtr< Gdk::Drop > &drop, void *ctx);
+   static Gdk::DragAction OnGtkDragEnter(double x, double y, void *ctx);
+   static Gdk::DragAction OnGtkDragMotion(double x, double y, void *ctx);
+   static bool OnGtkDragDrop(const Glib::ValueBase& value, double x, double y, void *ctx);
+   static void OnGtkDragLeave(void *ctx);
+   static void OnGtkDragDataReceived(void *ctx);
+   /*
+    * Target function for GH DnD. Makes call to common layer.
+    */
+   void TargetDragEnter();
+
+   /*
+    * Misc methods.
+    */
+   void InitGtk();
+
+   bool SetCPClipboardFromGtk_File(utf::string &source);
+   bool SetCPClipboardFromGtk_PlainText(utf::string &source);
+   bool SetCPClipboardFromGtk_RTF(utf::string &source);
+   bool SetCPClipboardFromGtk(std::string &data, std::string &drop_type);
+
+   utf::string GetLastDirName(const utf::string &str);
+   utf::utf8string GetNextPath(utf::utf8string &str, size_t& index);
+
+   static unsigned long GetTimeInMillis();
+
+   static inline bool TargetIsPlainText(const utf::string& target) {
+      return    target == TARGET_NAME_STRING
+             || target == TARGET_NAME_TEXT_PLAIN
+             || target == TARGET_NAME_UTF8_STRING
+             || target == TARGET_NAME_COMPOUND_TEXT;
+   }
+
+   static inline bool TargetIsRichText(const utf::string& target) {
+      return    target == TARGET_NAME_APPLICATION_RTF
+             || target == TARGET_NAME_TEXT_RICHTEXT
+             || target == TARGET_NAME_TEXT_RTF;
+   }
+
+   void UpdateWorkArea();
+
+   ToolsAppCtx *mCtx;
+   GuestDnDMgr *mDnD;
+   utf::string mHGStagingDir;
+   std::vector<utf::string> mHGFileContentsList;
+   DragDetWnd *mDetWnd;
+   CPClipboard mClipboard;
+   DnDBlockControl *mBlockCtrl;
+   DND_FILE_TRANSFER_STATUS mHGGetFileStatus;
+   bool mBlockAdded;
+
+   /* State to determine if drag motion is a drag enter. */
+   bool mGHDnDInProgress;
+   /* Icon updates from the guest. */
+   /* Only update mouse when we have clipboard contents from the host. */
+   bool mGHDnDDataReceived;
+   bool mInHGDrag;
+   // Flag to indicate if H->G drag has been triggered in guest by simulate mouse press
+   bool mHGDragStarted;
+   DND_DROPEFFECT mEffect;
+   int32 mMousePosX;
+   int32 mMousePosY;
+   int mNumPendingRequest;
+   unsigned long mDestDropTime;
+   uint64 mTotalFileSize;
+
+   /* GdkSurface used in dnd */
+   Glib::RefPtr< Gdk::Surface > mGdkSurface;
+   Glib::RefPtr<Gdk::ContentProvider> mProvider;
+   utf::string mHGDndUriList;
+   utf::utf8string mHGFCPData;
+
+   /*
+    * Upper left corner of our work area, a safe place for us to place
+    * our detection window without clashing with a windows parented to the
+    * composite overlay window.
+    */
+   int mOriginX;
+   int mOriginY;
+};
+
+#endif // __DND_UI_X11_H__
diff --git a/open-vm-tools/services/plugins/dndcp/dndGuestBase/dragDetWndX11GTK4.h b/open-vm-tools/services/plugins/dndcp/dndGuestBase/dragDetWndX11GTK4.h
new file mode 100644 (file)
index 0000000..e576d3e
--- /dev/null
@@ -0,0 +1,110 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+/**
+ * @file dragDetWndGTK4.h
+ *
+ *    Header for the DragDetWnd class.
+ */
+
+#ifndef DRAG_DET_WND_GTK4_H
+#define DRAG_DET_WND_GTK4_H
+
+#include <gtkmm.h>
+#include <gio/gio.h>
+#include <gdk/x11/gdkx.h>
+#include <X11/Xlib.h>
+
+#if !defined(DETWNDTEST)
+#include "dnd.h"
+#endif
+#include "fakeMouseWayland/fakeMouseWayland.h"
+
+using source_drag_begin_handler = void (const Glib::RefPtr< Gdk::Drag > &, void *);
+using source_drag_end_handler =  void (const Glib::RefPtr< Gdk::Drag > &, bool, void *);
+using source_drag_cancel_handler = bool (const Glib::RefPtr< Gdk::Drag > &, Gdk::DragCancelReason, void *);
+using source_drag_prepare_handler = Glib::RefPtr< Gdk::ContentProvider > (double, double, void *);
+using target_drag_accept_handler = bool (const Glib::RefPtr< Gdk::Drop > &, void *);
+using target_drag_drop_handler = bool (const Glib::ValueBase &, double, double, void *);
+using target_drag_enter_handler = Gdk::DragAction (double, double, void *);
+using target_drag_leave_handler = void (void *);
+using target_drag_motion_handler = Gdk::DragAction (double, double, void *);
+using target_drag_value_handler = void (void *);
+
+class DragDetWnd
+{
+public:
+   DragDetWnd(int UseUInput_fd = -1);
+   virtual ~DragDetWnd();
+   GtkWindow * GetWnd_gobj();
+   Gtk::Widget *GetWnd();
+
+   bool register_source_drag_begin_handler(source_drag_begin_handler handler, void *ctx);
+   bool register_source_drag_end_handler(source_drag_end_handler handler, void *ctx);
+   bool register_source_drag_cancel_handler(source_drag_cancel_handler handler, void *ctx);
+   bool register_source_drag_prepare_handler(source_drag_prepare_handler handler, void *ctx);
+   bool register_target_drag_accept_handler(target_drag_accept_handler handler, void *ctx);
+   bool register_target_drag_enter_handler(target_drag_enter_handler handler, void *ctx);
+   bool register_target_drag_drop_handler(target_drag_drop_handler handler, void *ctx);
+   bool register_target_drag_leave_handler(target_drag_leave_handler handler, void *ctx);
+   bool register_target_drag_motion_handler(target_drag_motion_handler handler, void *ctx);
+   bool register_target_value_handler(target_drag_value_handler handler, void *ctx);
+   bool EnableDND(void);
+   bool DisableDND(void);
+   void UpdateDetWnd(bool Show, int32 x, int32 y);
+   bool SimulateXEvents(const bool showWidget, const bool buttonEvent,
+                        const bool buttonPress, const bool moveWindow,
+                        const bool coordsProvided,
+                        const int xCoord, const int yCoord);
+   void StartDrag(Glib::RefPtr<Gdk::ContentProvider> provider,
+                  int mouseX,
+                  int mouseY);
+   bool Is_DropValue_Ready(void);
+   bool Get_Drop_Value(std::string &value, std::string &value_type);
+   bool Is_Drop_Supported(const Glib::RefPtr< Gdk::Drop > &drop);
+   bool Is_Current_Drop_Supported(void);
+   unsigned long Get_Current_Drag_ctx(void);
+   unsigned long Get_Current_Drop_ctx(void);
+   unsigned long Get_Detwnd_ctx(void);
+
+private:
+   void Show();
+   void Hide();
+   void Raise();
+   void Lower();
+   void Flush();
+   int GetScreenWidth();
+   int GetScreenHeight();
+   void SetGeometry(const int x, const int y,
+                    const int width, const int height);
+   void GetGeometry(int &x, int &y, int &width, int &height);
+   void SetIsVisible(const bool isVisible) {m_isVisible = isVisible;};
+   bool GetIsVisible() {return m_isVisible;};
+   bool SimulateMouseMove(const int x, const int y);
+
+   bool m_isVisible;
+   Gtk::Window *mWnd;
+   Glib::RefPtr< Gtk::DragSource > mSource;
+   Glib::RefPtr< Gtk::DropTarget > mTarget;
+   bool mUseUInput;
+   int mScreenWidth;
+   int mScreenHeight;
+};
+
+#endif // DRAG_DET_WND_H
index 42f5b4f880dfcad6424383f4d5e99887537a8c07..f04b4925e5833fe1c2417b7997fc06e510ae37be 100644 (file)
@@ -40,7 +40,12 @@ extern "C" {
    #include "vmware/tools/plugin.h"
 }
 
+#ifdef GTK4
+/* Need to use higher timeout value in GTK4, otherwise G->H dnd will fail */
+#define UNGRAB_TIMEOUT 3000    // 3.0s
+#else
 #define UNGRAB_TIMEOUT 500    // 0.5s
+#endif
 #define HIDE_DET_WND_TIMER 500    // 0.5s
 #define UNITY_DND_DET_TIMEOUT 500 // 0.5s
 
index 0f2c31cce9270bf7eee17015e6bf44a52f97a40d..f60f723cf65b52e8403824ade3d623675bc5a48b 100644 (file)
 
 #define VMTOOLS_USE_LEGACY_GTK
 
+#ifdef GTK4
+#undef VMTOOLS_USE_LEGACY_GTK
+#endif
 
 #include <X11/Xlib.h>
 #include <gtk/gtk.h>
+#ifdef GTK4
+#include <gdk/x11/gdkx.h>
+#endif
 
 #ifdef VMTOOLS_USE_LEGACY_GTK
 #include <gdk/gdkx.h>
@@ -44,5 +50,8 @@
 extern Display *gXDisplay;
 extern Window gXRoot;
 extern GtkWidget *gUserMainWidget;
+#ifdef GTK4
+extern GdkDisplay *gGdkDisplay;
+#endif
 
 #endif
diff --git a/open-vm-tools/services/plugins/dndcp/dndUIX11GTK4.cpp b/open-vm-tools/services/plugins/dndcp/dndUIX11GTK4.cpp
new file mode 100644 (file)
index 0000000..2a0e160
--- /dev/null
@@ -0,0 +1,1883 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+/**
+ * @file dndUIX11GTK4.cpp --
+ *
+ * This class implements stubs for the methods that allow DnD between
+ * host and guest on GTK4 lib.
+ */
+
+#define G_LOG_DOMAIN "dndcp"
+
+#include "xutils/xutilsGTK4.hh"
+#include "copyPasteDnDUtil.h"
+#include "dndUIX11GTK4.h"
+#include "guestDnDCPMgr.hh"
+#include "tracer.hh"
+#include "fakeMouseWayland/fakeMouseWayland.h"
+
+extern "C" {
+#include "vmware/guestrpc/tclodefs.h"
+
+#include "copyPasteCompat.h"
+#include "cpName.h"
+#include "cpNameUtil.h"
+#include "dndClipboard.h"
+#include "hgfsUri.h"
+#include "rpcout.h"
+}
+
+#include "dnd.h"
+#include "dndMsg.h"
+#include "hostinfo.h"
+#include "file.h"
+#include "vmblock.h"
+
+/* IsXExtensionPointer may be not defined with old Xorg. */
+#ifndef IsXExtensionPointer
+#define IsXExtensionPointer 4
+#endif
+
+#include "copyPasteDnDWrapper.h"
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::DnDUIX11 --
+ *
+ *      Constructor.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+DnDUIX11::DnDUIX11(ToolsAppCtx *ctx)
+    : mCtx(ctx),
+      mDnD(NULL),
+      mDetWnd(NULL),
+      mClipboard(),
+      mBlockCtrl(NULL),
+      mHGGetFileStatus(DND_FILE_TRANSFER_NOT_STARTED),
+      mBlockAdded(false),
+      mGHDnDInProgress(false),
+      mGHDnDDataReceived(false),
+      mInHGDrag(false),
+      mEffect(DROP_NONE),
+      mMousePosX(0),
+      mMousePosY(0),
+      mNumPendingRequest(0),
+      mDestDropTime(0),
+      mTotalFileSize(0),
+      mOriginX(0),
+      mOriginY(0),
+      mGdkSurface(NULL)
+{
+   TRACE_CALL();
+
+
+   /*
+    * XXX Hard coded use of default screen means this doesn't work in dual-
+    * headed setups (e.g. DISPLAY=:0.1).  However, the number of people running
+    * such setups in VMs is expected to be, like, hella small, so I'mma cut
+    * corners for now.
+    */
+   Glib::RefPtr<Gdk::Display> display = Gdk::Display::get_default();
+   GdkSurface *gdkSurface = gdk_x11_display_get_default_group(display->gobj());
+   mGdkSurface = Glib::wrap(gdkSurface);
+   UpdateWorkArea();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::~DnDUIX11 --
+ *
+ *      Destructor.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+DnDUIX11::~DnDUIX11()
+{
+   TRACE_CALL();
+
+   if (mDetWnd) {
+      delete mDetWnd;
+      mDetWnd = NULL;
+   }
+   CPClipboard_Destroy(&mClipboard);
+   /* Any files from last unfinished file transfer should be deleted. */
+   if (DND_FILE_TRANSFER_IN_PROGRESS == mHGGetFileStatus
+       && !mHGStagingDir.empty()) {
+      uint64 totalSize = File_GetSizeEx(mHGStagingDir.c_str());
+      if (mTotalFileSize != totalSize) {
+         g_debug("%s: deleting %s, expecting %" FMT64 "u, finished %" FMT64 "u\n",
+                 __FUNCTION__, mHGStagingDir.c_str(),
+                 mTotalFileSize, totalSize);
+         DnD_DeleteStagingFiles(mHGStagingDir.c_str(), FALSE);
+      } else {
+         g_debug("%s: file size match %s\n",
+                 __FUNCTION__, mHGStagingDir.c_str());
+      }
+   }
+   ResetUI();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::Init --
+ *
+ *      Initialize DnDUIX11 object.
+ *
+ * Results:
+ *      Returns true on success and false on failure.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+DnDUIX11::Init()
+{
+   TRACE_CALL();
+   bool ret = true;
+
+   CPClipboard_Init(&mClipboard);
+
+   GuestDnDCPMgr *p = GuestDnDCPMgr::GetInstance();
+   ASSERT(p);
+   mDnD = p->GetDnDMgr();
+   ASSERT(mDnD);
+
+   mDetWnd = new DragDetWnd(mCtx->uinputFD);
+   if (!mDetWnd) {
+      g_debug("%s: unable to allocate DragDetWnd object\n", __FUNCTION__);
+      goto fail;
+   }
+
+   InitGtk();
+
+#define CONNECT_SIGNAL(_obj, _sig, _cb) \
+   _obj->_sig.connect(sigc::mem_fun(this, &DnDUIX11::_cb))
+   /* Set common layer callbacks. */
+   CONNECT_SIGNAL(mDnD, srcDragBeginChanged,   OnSrcDragBegin);
+   CONNECT_SIGNAL(mDnD, srcCancelChanged,      OnSrcCancel);
+   CONNECT_SIGNAL(mDnD, getFilesDoneChanged,   OnGetFilesDone);
+   CONNECT_SIGNAL(mDnD, moveMouseChanged,      OnMoveMouse);
+   CONNECT_SIGNAL(mDnD, srcDropChanged,        OnSrcDrop);
+   CONNECT_SIGNAL(mDnD, destCancelChanged,     OnDestCancel);
+   CONNECT_SIGNAL(mDnD, destMoveDetWndToMousePosChanged, OnDestMoveDetWndToMousePos);
+   CONNECT_SIGNAL(mDnD, privDropChanged,       OnPrivateDrop);
+   CONNECT_SIGNAL(mDnD, updateDetWndChanged,   OnUpdateDetWnd);
+   /* TODO: Unity related functions are deprecated, remove it later */
+   CONNECT_SIGNAL(mDnD, updateUnityDetWndChanged, OnUpdateUnityDetWnd);
+#undef CONNECT_SIGNAL
+
+   mDetWnd->UpdateDetWnd(false, 0, 0);
+   goto out;
+fail:
+   ret = false;
+   if (mDnD) {
+      delete mDnD;
+      mDnD = NULL;
+   }
+   if (mDetWnd) {
+      delete mDetWnd;
+      mDetWnd = NULL;
+   }
+out:
+   return ret;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::InitGtk --
+ *
+ *      Register supported DND target types and signal handlers with GTK+.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::InitGtk()
+{
+   TRACE_CALL();
+
+   mDetWnd->register_source_drag_begin_handler(OnGtkSrcDragBegin, (void *)this);
+   mDetWnd->register_source_drag_end_handler(OnGtkSrcDragEnd, (void *)this);
+   mDetWnd->register_source_drag_cancel_handler(OnGtkSrcDragCancel, (void *)this);
+   mDetWnd->register_target_drag_accept_handler(OnGtkDragAccept, (void *)this);
+   mDetWnd->register_target_drag_enter_handler(OnGtkDragEnter, (void *)this);
+   mDetWnd->register_target_drag_drop_handler(OnGtkDragDrop, (void *)this);
+   mDetWnd->register_target_drag_leave_handler(OnGtkDragLeave, (void *)this);
+   mDetWnd->register_target_drag_motion_handler(OnGtkDragMotion, (void *)this);
+   mDetWnd->register_target_value_handler(OnGtkDragDataReceived, (void *)this);
+   mDetWnd->EnableDND();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::ResetUI --
+ *
+ *      Reset UI state variables.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      May remove a vmblock blocking entry.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::ResetUI()
+{
+   TRACE_CALL();
+   mGHDnDDataReceived = false;
+   mHGGetFileStatus = DND_FILE_TRANSFER_NOT_STARTED;
+   mGHDnDInProgress = false;
+   mEffect = DROP_NONE;
+   mInHGDrag = false;
+   RemoveBlock();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::AddBlock --
+ *
+ *      Insert a vmblock blocking entry.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Caller must pair with RemoveBlock() upon dnd completion/cancellation.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::AddBlock()
+{
+   TRACE_CALL();
+
+   if (mBlockAdded) {
+      g_debug("%s: block already added\n", __FUNCTION__);
+      return;
+   }
+
+   g_debug("%s: DnDBlockIsReady %d fd %d\n", __FUNCTION__,
+           DnD_BlockIsReady(mBlockCtrl), mBlockCtrl->fd);
+
+   if (   DnD_BlockIsReady(mBlockCtrl)
+       && mBlockCtrl->AddBlock(mBlockCtrl->fd, mHGStagingDir.c_str())) {
+      mBlockAdded = true;
+      g_debug("%s: add block for %s.\n", __FUNCTION__, mHGStagingDir.c_str());
+   } else {
+      mBlockAdded = false;
+      g_debug("%s: unable to add block dir %s.\n", __FUNCTION__, mHGStagingDir.c_str());
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::RemoveBlock --
+ *
+ *      Remove a vmblock blocking entry.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::RemoveBlock()
+{
+   TRACE_CALL();
+
+   if (mBlockAdded && (DND_FILE_TRANSFER_IN_PROGRESS != mHGGetFileStatus)) {
+      g_debug("%s: removing block for %s\n", __FUNCTION__, mHGStagingDir.c_str());
+      /* We need to make sure block subsystem has not been shut off. */
+      if (DnD_BlockIsReady(mBlockCtrl)) {
+         mBlockCtrl->RemoveBlock(mBlockCtrl->fd, mHGStagingDir.c_str());
+      }
+      mBlockAdded = false;
+   } else {
+      g_debug("%s: not removing block mBlockAdded %d mHGGetFileStatus %d\n",
+              __FUNCTION__, mBlockAdded, mHGGetFileStatus);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::LocalSetContentProvider --
+ *
+ *      Set content provider based on different desktop type.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      mProvider is chanaged.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::LocalSetContentProvider(const char* desktop)   // IN
+{
+   std::vector<Glib::RefPtr<Gdk::ContentProvider>> providerVec;
+   Glib::RefPtr<Glib::Bytes> fileListGnome;
+   Glib::RefPtr<Glib::Bytes> fileListKde;
+   void *buf;
+   size_t sz;
+
+   g_debug("%s: enter.\n", __FUNCTION__);
+
+   if (CPClipboard_ItemExists(&mClipboard, CPFORMAT_FILELIST) ||
+       CPClipboard_ItemExists(&mClipboard, CPFORMAT_FILECONTENTS)) {
+      if (desktop && strstr(desktop, "GNOME")) {
+         // Set the Gnome file list to GDKContentProvider
+         fileListGnome = Glib::Bytes::create(mHGDndUriList.c_str(), mHGDndUriList.bytes());
+         providerVec.push_back(Gdk::ContentProvider::create(DRAG_TARGET_NAME_URI_LIST, fileListGnome));
+      } else if (desktop && strstr(desktop, "KDE")) {
+         // Set the Kde file list to GDKContentProvider
+         fileListKde = Glib::Bytes::create(mHGDndUriList.c_str(), mHGDndUriList.bytes());
+         providerVec.push_back(Gdk::ContentProvider::create(DRAG_TARGET_NAME_URI_LIST, fileListKde));
+      } else {
+         g_warning("%s: Unsupported Desktop Manager %s. \n", __FUNCTION__, desktop);
+         return;
+      }
+   } else if (CPClipboard_ItemExists(&mClipboard, CPFORMAT_RTF)) {
+      if (CPClipboard_GetItem(&mClipboard, CPFORMAT_RTF, &buf, &sz)) {
+         g_debug("%s: RTF data, size %" FMTSZ "u.\n", __FUNCTION__, sz);
+         Glib::RefPtr<Glib::Bytes> rtfData = Glib::Bytes::create(buf, sz);
+         providerVec.push_back(Gdk::ContentProvider::create(TARGET_NAME_APPLICATION_RTF, rtfData));
+         providerVec.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_RICHTEXT, rtfData));
+         providerVec.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_RTF, rtfData));
+      }
+   } else if (CPClipboard_ItemExists(&mClipboard, CPFORMAT_TEXT)) {
+      if (CPClipboard_GetItem(&mClipboard, CPFORMAT_TEXT, &buf, &sz)) {
+         g_debug("%s: Text data, size %" FMTSZ "u.\n", __FUNCTION__, sz);
+         Glib::RefPtr<Glib::Bytes> textData = Glib::Bytes::create(buf, sz);
+         providerVec.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_PLAIN, textData));
+         providerVec.push_back(Gdk::ContentProvider::create(TARGET_NAME_TEXT_PLAIN_UTF8, textData));
+      }
+   }
+   mProvider = Gdk::ContentProvider::create(providerVec);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::BuildFileURIList --
+ *
+ *      Build URI file list with proper pre/post str.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::BuildFileURIList(utf::string& uriList,         // IN/OUT
+                           utf::string pre,              // IN
+                           utf::string post,             // IN
+                           utf::string stagingDirName)   // IN
+{
+   size_t pos  = 0;
+   utf::string str;
+
+   while ((str = GetNextPath(mHGFCPData, pos).c_str()).bytes() != 0) {
+      g_debug("%s: Path: %s", __FUNCTION__, str.c_str());
+      uriList += pre;
+      if (mBlockAdded) {
+         uriList += mBlockCtrl->blockRoot;
+         uriList += DIRSEPS + stagingDirName + DIRSEPS + str + post;
+      } else {
+         uriList += DIRSEPS + mHGStagingDir + DIRSEPS + str + post;
+      }
+   }
+   g_debug("%s: providing uriList [%s]\n", __FUNCTION__, uriList.c_str());
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::BuildFileContentURIList --
+ *
+ *      Used to populate the file content URI lists for GNOME, Nautilus, and KDE.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::BuildFileContentURIList(utf::string& uriList,         // IN/OUT
+                                        utf::string pre,              // IN
+                                        utf::string post)             // IN
+{
+   for (const auto& path : mHGFileContentsList) {
+      uriList += pre + path + post;
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::LocalGetFileRequest --
+ *
+ *      Build URI file list with proper pre/post str.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      mHGDndUriList may be changed.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::LocalGetFileRequest(utf::string stagingDirName,
+                              const char *desktop)
+{
+   mHGDndUriList = "";
+   if (desktop && strstr(desktop, "GNOME")) {
+      /* Setting Gnome URIs for each path in the guest's file list. */
+      BuildFileURIList(mHGDndUriList, FCP_GNOME_LIST_PRE, FCP_GNOME_LIST_POST, stagingDirName);
+      /* Nautilus on Gnome does not expect FCP_GNOME_LIST_POST after the last uri. See bug 143147. */
+      mHGDndUriList.erase(mHGDndUriList.size() - strlen(FCP_GNOME_LIST_POST));
+      g_debug("%s: providing file list [%s]\n", __FUNCTION__, mHGDndUriList.c_str());
+   } else if (desktop && strstr(desktop, "KDE")) {
+      /* Setting KDE URIs for each path in the guest's file list. */
+      BuildFileURIList(mHGDndUriList, DND_URI_LIST_PRE_KDE, DND_URI_LIST_POST, stagingDirName);
+      g_debug("%s: providing file list [%s]\n", __FUNCTION__, mHGDndUriList.c_str());
+   } else {
+      g_warning("%s: Unsupported Desktop Manager %s. \n", __FUNCTION__, desktop);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DndUIX11::LocalGetFileContentsRequest --
+ *
+ *      Build the files content uri list when host dnd with file content.
+ *      H->G only.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      mHGDndUriList may be changed.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::LocalGetFileContentsRequest(const char *desktop)
+{
+   mHGDndUriList = "";
+   if (desktop && strstr(desktop, "GNOME")) {
+      /* Setting Gnome URIs for each path in the guest's file list. */
+      BuildFileContentURIList(mHGDndUriList, FCP_GNOME_LIST_PRE, FCP_GNOME_LIST_POST);
+      /* Nautilus on Gnome does not expect FCP_GNOME_LIST_POST after the last uri. See bug 143147. */
+      mHGDndUriList.erase(mHGDndUriList.size() - strlen(FCP_GNOME_LIST_POST));
+      g_debug("%s: providing file list [%s]\n", __FUNCTION__, mHGDndUriList.c_str());
+   } else if (desktop && strstr(desktop, "KDE")) {
+      /* Setting KDE URIs for each path in the guest's file list. */
+      BuildFileContentURIList(mHGDndUriList, DND_URI_LIST_PRE_KDE, DND_URI_LIST_POST);
+      g_debug("%s: providing file list [%s]\n", __FUNCTION__, mHGDndUriList.c_str());
+   } else {
+      g_warning("%s: Unsupported Desktop Manager %s. \n", __FUNCTION__, desktop);
+   }
+}
+
+
+/* Source functions for HG DnD. */
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnSrcDragBegin --
+ *
+ *      Called when host successfully detected a pending HG drag.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Calls mDetWnd->drag_begin().
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnSrcDragBegin(const CPClipboard *clip,       // IN
+                         const std::string stagingDir)  // IN
+{
+   int mouseX = mOriginX + DRAG_DET_WINDOW_WIDTH / 2;
+   int mouseY = mOriginY + DRAG_DET_WINDOW_WIDTH / 2;
+   const char *desktop = getenv("XDG_CURRENT_DESKTOP");
+   bool hasContent = false;
+   mHGDragStarted = false;
+
+   /* Move detect window to the mouse in position */
+   OnUpdateDetWnd(true, mOriginX, mOriginY);
+
+   TRACE_CALL();
+
+   CPClipboard_Clear(&mClipboard);
+   CPClipboard_Copy(&mClipboard, clip);
+
+   /*
+    * Construct the target and action list, as well as a fake motion notify
+    * event that's consistent with one that would typically start a drag.
+    */
+   utf::string stagingDirName;
+   DnDFileList fList;
+   void *buf;
+   size_t sz;
+
+   mHGFCPData.clear();
+
+   if (CPClipboard_GetItem(&mClipboard, CPFORMAT_FILELIST, &buf, &sz)) {
+      mHGStagingDir = utf::string(stagingDir.c_str());
+
+      /* Provide path within vmblock file system instead of actual path. */
+      stagingDirName = GetLastDirName(mHGStagingDir);
+      if (stagingDirName.length() == 0) {
+         g_debug("%s: Cannot get staging directory name, stagingDir: %s\n",
+                 __FUNCTION__, mHGStagingDir.c_str());
+         return;
+      }
+
+      if (!fList.FromCPClipboard(buf, sz)) {
+         g_debug("%s: Can't get data from clipboard\n", __FUNCTION__);
+         return;
+      }
+      mTotalFileSize = fList.GetFileSize();
+      // Provide path within vmblock file system instead of actual path.
+      mHGFCPData = fList.GetRelPathsStr();
+
+      if (!mBlockAdded) {
+         AddBlock();
+      } else {
+         g_debug("%s: not calling AddBlock\n", __FUNCTION__);
+      }
+      if (mHGGetFileStatus != DND_FILE_TRANSFER_IN_PROGRESS) {
+         mHGGetFileStatus = DND_FILE_TRANSFER_IN_PROGRESS;
+      }
+
+      LocalGetFileRequest(stagingDirName, desktop);
+      hasContent = true;
+   }
+
+   if (CPClipboard_ItemExists(&mClipboard, CPFORMAT_FILECONTENTS)) {
+      g_debug("%s: Get file content from host clipboard", __FUNCTION__);
+      LocalGetFileContentsRequest(desktop);
+      hasContent = true;
+   }
+
+   if (CPClipboard_ItemExists(&mClipboard, CPFORMAT_TEXT) ||
+       CPClipboard_ItemExists(&mClipboard, CPFORMAT_RTF)) {
+      g_debug("%s: Get text/rtfText content from host clipboard", __FUNCTION__);
+      hasContent = true;
+   }
+
+   /* finally set the guest clipboard */
+   if (hasContent) {
+      LocalSetContentProvider(desktop);
+   }
+
+   SourceDragStartDone();
+   /* Initialize host hide feedback to DROP_NONE. */
+   mEffect = DROP_NONE;
+   SourceUpdateFeedback(mEffect);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnMoveMouse --
+ *
+ *      simulate mouse move for h->g dnd.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnMoveMouse(int32 x,  // IN: Pointer x-coord
+                      int32 y)  // IN: Pointer y-coord
+{
+   // Position the pointer, and record its position.
+   mDetWnd->SimulateXEvents(false, false, false, false, true, x, y);
+   mMousePosX = x;
+   mMousePosY = y;
+
+   if (mInHGDrag) {
+      if (!mHGDragStarted) {
+         /*
+          * We want to simulate the mouse press and hold to trigger drag_begin
+          * event for GtkDragSource, do it here rather than SrcDragBegin as
+          * we want to get the mouse position.
+          *
+          * Before the DnD, we should make sure that the mouse is released
+          * otherwise it may be another DnD, not ours. Send a release, then
+          * a press here to cover this case.
+          */
+         mDetWnd->SimulateXEvents(true, true, false, true, true, x, y);
+         // simulate drag mouse press
+         mDetWnd->SimulateXEvents(false, true, true, false, true, x, y);
+         // Tell Gtk that a drag should be started from this widget.
+         mDetWnd->StartDrag(mProvider, x, y);
+         mHGDragStarted = true;
+      }
+      if (mEffect != DROP_MOVE) {
+         mEffect = DROP_MOVE;
+         g_debug("%s: Updating feedback\n", __FUNCTION__);
+         SourceUpdateFeedback(mEffect);
+      }
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnSrcCancel --
+ *
+ *      Handler for when host cancels HG drag.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Detection window and fake mouse events.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnSrcCancel()
+{
+   TRACE_CALL();
+   /*
+    * Force the window to show, position the mouse over it, and release.
+    * Seems like moving the window to 0, 0 eliminates frequently observed
+    * flybacks when we cancel as user moves mouse in and out of destination
+    * window in a H->G DnD.
+    */
+   OnUpdateDetWnd(true, mOriginX, mOriginY);
+   mDetWnd->SimulateXEvents(true, true, false, true, true,
+                            mOriginX + DRAG_DET_WINDOW_WIDTH / 2,
+                            mOriginY + DRAG_DET_WINDOW_WIDTH / 2);
+   OnUpdateDetWnd(false, 0, 0);
+   mDetWnd->SimulateXEvents(false, false, false, false, true,
+                           mMousePosX, mMousePosY);
+   mInHGDrag = false;
+   mHGGetFileStatus = DND_FILE_TRANSFER_NOT_STARTED;
+   mEffect = DROP_NONE;
+   RemoveBlock();
+}
+
+
+/* Source functions for GH DnD. */
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnPrivateDrop --
+ *
+ *      Handler for private drop event.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Releases mouse button at current position.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnPrivateDrop(int32 x,        // UNUSED
+                        int32 y)        // UNUSED
+{
+   TRACE_CALL();
+
+   /* Unity manager in host side may already send the drop into guest. */
+   if (mGHDnDInProgress) {
+
+      /*
+       * Release the mouse button.
+       */
+      mDetWnd->SimulateXEvents(false, true, false, false, false, 0, 0);
+   }
+   ResetUI();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnDestCancel --
+ *
+ *      Handler for GH drag cancellation.
+ *
+ *      Note: This event fires as part of the complete guest-to-host sequence,
+ *      not just error or user cancellation.
+ *
+ * Results:
+ *      Uses detection window and fake mouse events to intercept drop.
+ *
+ * Side effects:
+ *      Reinitializes UI state.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnDestCancel()
+{
+   TRACE_CALL();
+
+   /* Unity manager in host side may already send the drop into guest. */
+   if (mGHDnDInProgress) {
+      Glib::RefPtr<Gdk::Display> display = Gdk::Display::get_default();
+      GdkSurface *gdkSurface = gdk_x11_display_get_default_group(display->gobj());
+      UpdateWorkArea();
+
+     /*
+       * Show the window, move it to the mouse position, and release the
+       * mouse button.
+       */
+      mDetWnd->SimulateXEvents(true, true, false, true, false,
+                               mOriginX, mOriginY);
+   }
+   mDestDropTime = GetTimeInMillis();
+   ResetUI();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnSrcDrop --
+ *
+ *      Callback when host signals drop.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Release the mouse button in the detection window.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnSrcDrop()
+{
+   TRACE_CALL();
+   OnUpdateDetWnd(true, mOriginX, mOriginY);
+
+   /*
+    * Move the mouse to the saved coordinates, and release the mouse button.
+    */
+   mDetWnd->SimulateXEvents(false, true, false, false, true,
+                            mMousePosX, mMousePosY);
+   OnUpdateDetWnd(false, 0, 0);
+   /* We've successful or failure for HG drag, set the flag to false anyway */
+   mInHGDrag = false;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGetFilesDone --
+ *
+ *      Callback when HG file transfer completes.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Releases vmblock blocking entry.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnGetFilesDone(bool success)  // IN: true if transfer succeeded
+{
+   g_debug("%s: %s\n", __FUNCTION__, success ? "success" : "failed");
+
+   /*
+    * If H->G drag is not done yet, only remove block. Otherwise destination
+    * may miss the data because we are already reset.
+    */
+
+   mHGGetFileStatus = DND_FILE_TRANSFER_FINISHED;
+
+   if (!mInHGDrag) {
+      ResetUI();
+   } else {
+      RemoveBlock();
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnUpdateDetWnd --
+ *
+ *      Callback to show/hide drag detection window.
+ *
+ * Results:
+ *      Shows/hides and moves detection window.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnUpdateDetWnd(bool show,     // IN: show (true) or hide (false)
+                         int32 x,       // IN: detection window's destination x-coord
+                         int32 y)       // IN: detection window's destination y-coord
+{
+   g_debug("%s: enter 0x%lx show %d x %d y %d\n",
+           __FUNCTION__,
+           mDetWnd->Get_Current_Drop_ctx(), show, x, y);
+
+   /* If the window is being shown, move it to the right place. */
+   if (show) {
+      UpdateWorkArea();
+      x = MAX(x - DRAG_DET_WINDOW_WIDTH / 2, mOriginX);
+      y = MAX(y - DRAG_DET_WINDOW_WIDTH / 2, mOriginY);
+      mDetWnd->UpdateDetWnd(show, x, y);
+   } else {
+      g_debug("%s: hide\n", __FUNCTION__);
+      mDetWnd->UpdateDetWnd(show, x, y);
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnUpdateUnityDetWnd --
+ *
+ *      Callback to show/hide fullscreen Unity drag detection window.
+ *
+ *      TODO: Unity related functions are deprecated, remove it later once
+ *            backend RPC functions are removed.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Detection window shown, hidden.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnUpdateUnityDetWnd(bool show,         // IN: show (true) or hide (false)
+                              uint32 unityWndId, // IN: XXX ?
+                              bool bottom)       // IN: place window at bottom of stack?
+{
+   NOT_IMPLEMENTED();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnDestMoveDetWndToMousePos --
+ *
+ *      Callback to move detection window to current moue position.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      Detection window is moved, shown.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnDestMoveDetWndToMousePos()
+{
+   mDetWnd->SimulateXEvents(true, false, true, true, false, 0, 0);
+}
+
+
+/*
+ ****************************************************************************
+ * BEGIN GTK+ Callbacks (dndcp as drag source: host-to-guest)
+ */
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkSrcDragBegin --
+ *
+ *      GTK4 h->g drag begin handler
+ *
+ * Results:
+ *      None
+ *
+ * Side effects:
+ *      Signals to drag source that drag is started.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnGtkSrcDragBegin(const Glib::RefPtr< Gdk::Drag > &drag, void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   g_debug("%s: DND drag begin with ctx %ld\n", __FUNCTION__,
+           this_instance->mDetWnd->Get_Current_Drag_ctx());
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkSrcDragEnd --
+ *
+ *      GTK4 h->g drag end handler
+ *
+ * Results:
+ *      None
+ *
+ * Side effects:
+ *      Signals to drag source that drag is started.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnGtkSrcDragEnd(const Glib::RefPtr< Gdk::Drag > &drag,
+                          bool delete_data,
+                          void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   g_debug("%s: DND drag done with ctx %ld\n", __FUNCTION__,
+           this_instance->mDetWnd->Get_Current_Drag_ctx());
+   if (this_instance->mHGGetFileStatus != DND_FILE_TRANSFER_IN_PROGRESS) {
+      g_debug("%s: reset UI\n", __FUNCTION__);
+      this_instance->ResetUI();
+   }
+   this_instance->mInHGDrag = false;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkSrcDragCancel --
+ *
+ *      GTK+ DragSource "drag_leave" signal handler.
+ *
+ *      Logs event.  Acknowledges, finishes outdated sequence if drag context
+ *      is not the same as we're currently interested in (i.e. != mDragCtx).
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      May cancel dnd associated with this drag context.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+DnDUIX11::OnGtkSrcDragCancel(const Glib::RefPtr< Gdk::Drag > &drag,
+                             Gdk::DragCancelReason reason, void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   g_debug("%s: DND cancel for drag %ld with reason %d\n", __FUNCTION__,
+           this_instance->mDetWnd->Get_Current_Drag_ctx(), (int)reason);
+   this_instance->ResetUI();
+   return true;
+}
+
+
+/*
+ * END GTK+ Callbacks (dndcp as drag source: host-to-guest)
+ ****************************************************************************
+ */
+
+
+/*
+ ****************************************************************************
+ * BEGIN GTK+ Callbacks (dndcp as drag destination: guest-to-host)
+ */
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkDragAccept --
+ *
+ *      GTK4 "signal_accept" signal handler for DropTarget.
+ *
+ * Results:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+DnDUIX11::OnGtkDragAccept(const Glib::RefPtr< Gdk::Drop > &drop, void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   if (!this_instance->mDnD->IsDnDAllowed()) {
+      g_warning("%s: No dnd allowed!\n", __FUNCTION__);
+      return false;
+   }
+
+   if (!this_instance->mDetWnd->Is_Drop_Supported(drop)) {
+      g_warning("%s: No Unsupported DND type\n", __FUNCTION__);
+      return false;
+   }
+
+   if (this_instance->mInHGDrag) {
+      g_warning("%s: In H->G dnd process rather than G->H, exit\n", __FUNCTION__);
+      return false;
+   }
+
+   if (this_instance->mGHDnDInProgress) {
+      /*
+       * Errors in the GTK layer can lead to dropped information without
+       * notification. Therefore, a cleanup process is necessary.
+       */
+      g_warning("%s: Previous drag failed\n", __FUNCTION__);
+      this_instance->ResetUI();
+   }
+
+   g_debug("%s: new drag, need to get data for host\n", __FUNCTION__);
+   /*
+    * This is a new drag operation. We need to start a drag thru the
+    * RPC, and to the host. Before we can tell the host, we have to
+    * retrieve the drop data.
+    */
+   this_instance->mGHDnDInProgress = true;
+   /* only begin drag enter after we get the data */
+   /* Need to grab all of the data. */
+   CPClipboard_Clear(&this_instance->mClipboard);
+   this_instance->mNumPendingRequest = 0;
+   return true;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkDragEnter --
+ *
+ *       GTK4 "signal_enter" signal handler for DropTarget.
+ *
+ * Results:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+Gdk::DragAction
+DnDUIX11::OnGtkDragEnter(double x, // IN: drag motion x-coord
+                         double y, // IN: drag motion y-coord
+                         void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   Gdk::DragAction action;
+
+   if (this_instance->mDetWnd->Is_Current_Drop_Supported()) {
+      action = Gdk::DragAction::COPY;
+   } else {
+      g_warning("%s: No dnd allowed @ (%f, %f)!\n", __FUNCTION__, x, y);
+   }
+
+   return action;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkDragDrop --
+ *
+ *      GTK+ "drag_drop" signal handler.
+ *
+ * Results:
+ *      Returns true so long as drag target and data are (at one point)
+ *      provided (i.e. not a spurious event).
+ *
+ * Side effects:
+ *      Signals to drag source that drop is finished.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+DnDUIX11::OnGtkDragDrop(const Glib::ValueBase& value,
+                        double x,
+                        double y,
+                        void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   g_debug("%s: Ctx %ld, drop at x %f y %f\n", __FUNCTION__,
+           this_instance->mDetWnd->Get_Current_Drop_ctx(), x, y);
+
+   this_instance->ResetUI();
+
+   if (CPClipboard_IsEmpty(&this_instance->mClipboard)) {
+      g_debug("%s: No valid data on mClipboard.\n", __FUNCTION__);
+      return false;
+   }
+
+   return true;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkDragLeave --
+ *
+ *      GTK+ "drag_leave" signal handler.
+ *
+ *      Logs event.  Acknowledges, finishes outdated sequence if drag context
+ *      is not the same as we're currently interested in (i.e. != mDragCtx).
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      May cancel dnd associated with this drag context.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+ void
+ DnDUIX11::OnGtkDragLeave(void *ctx)
+ {
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+
+   g_debug("%s: DND leave for drop %ld\n", __FUNCTION__,
+           this_instance->mDetWnd->Get_Current_Drop_ctx());
+
+   if (this_instance->mGHDnDInProgress) {
+      g_debug("%s: reset UI\n", __FUNCTION__);
+      this_instance->ResetUI();
+   }
+ }
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkDragMotion --
+ *
+ *      GTK "drag_motion" signal handler.
+ *
+ *      We should respond by setting drag status. Note that there is no drag
+ *      enter signal. We need to figure out if a new drag is happening on
+ *      our own. Also, we don't respond with a "allowed" drag status right
+ *      away, we start a new drag operation with the host (which tries to
+ *      notify the host of the new operation). Once the host has responded),
+ *      we respond with a proper drag status.
+ *
+ *      Note: You may see this callback during DnD when detection window
+ *      is acting as a source. In that case it will be ignored. In a future
+ *      refactoring, we will try and avoid this.
+ *
+ * Results:
+ *      Returns true unless we don't recognize the types offered.
+ *
+ * Side effects:
+ *      Via RequestData issues a Gtk::Widget::drag_get_data.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+Gdk::DragAction
+DnDUIX11::OnGtkDragMotion(double x, // IN: drag motion x-coord
+                          double y, // IN: drag motion y-coord
+                          void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   Gdk::DragAction action = Gdk::DragAction::COPY;
+   /*
+    * If this is a Host to Guest drag, we are done here, so return.
+    */
+   unsigned long curTime = GetTimeInMillis();
+   g_debug("%s: enter dnd ctx %ld\n", __FUNCTION__,
+           this_instance->mDetWnd->Get_Current_Drop_ctx());
+   if (curTime - this_instance->mDestDropTime <= 1000) {
+      g_debug("%s: ignored %ld %ld %ld\n", __FUNCTION__,
+              curTime, this_instance->mDestDropTime,
+              curTime - this_instance->mDestDropTime);
+      goto out;
+   }
+
+   g_debug("%s: not ignored %ld %ld %ld\n", __FUNCTION__,
+           curTime, this_instance->mDestDropTime,
+           curTime - this_instance->mDestDropTime);
+
+   if (this_instance->mInHGDrag ||
+       (this_instance->mHGGetFileStatus != DND_FILE_TRANSFER_NOT_STARTED)) {
+      g_debug("%s: ignored not in hg drag or not getting hg data\n",
+              __FUNCTION__);
+      goto out;
+   }
+   if (!this_instance->mGHDnDInProgress) {
+      g_debug("%s: new drag, need to get data for host\n", __FUNCTION__);
+      /*
+       * This is a new drag operation. We need to start a drag thru the
+       * backdoor, and to the host. Before we can tell the host, we have to
+       * retrieve the drop data.
+       */
+      this_instance->mGHDnDInProgress = true;
+      /* only begin drag enter after we get the data */
+      /* Need to grab all of the data. */
+      CPClipboard_Clear(&this_instance->mClipboard);
+      this_instance->mNumPendingRequest = 0;
+   } else {
+      g_debug("%s: Multiple drag motions before gh data has been received.\n",
+              __FUNCTION__);
+   }
+out:
+   return action;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::OnGtkDragDataReceived --
+ *
+ *      GTK+ "drag_data_received" signal handler.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      May signal to host beginning of guest-to-host DND.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::OnGtkDragDataReceived(void *ctx)
+{
+   DnDUIX11 *this_instance = (DnDUIX11 *)ctx;
+   std::string value;
+   std::string value_type;
+
+   g_debug("%s: enter dnd ctx %ld\n", __FUNCTION__,
+           this_instance->mDetWnd->Get_Current_Drop_ctx());
+   /* The GH DnD may already finish before we got response. */
+   if (!this_instance->mGHDnDInProgress) {
+      g_debug("%s: not valid\n", __FUNCTION__);
+      return;
+   }
+
+   if (this_instance->mDetWnd->Is_DropValue_Ready()) {
+      if (this_instance->mDetWnd->Get_Drop_Value(value, value_type)) {
+         if (this_instance->SetCPClipboardFromGtk(value, value_type)) {
+            this_instance->mNumPendingRequest--;
+            if (this_instance->mNumPendingRequest > 0) {
+               return;
+            }
+            if (CPClipboard_IsEmpty(&this_instance->mClipboard)) {
+               g_debug("%s: Failed getting item.\n", __FUNCTION__);
+               this_instance->ResetUI();
+               return;
+            }
+            if (!this_instance->mGHDnDDataReceived) {
+               g_debug("%s: Drag entering.\n", __FUNCTION__);
+               this_instance->mGHDnDDataReceived = true;
+               this_instance->TargetDragEnter();
+            }
+         } else {
+            g_debug("%s: Failed to set CP clipboard.\n", __FUNCTION__);
+            this_instance->ResetUI();
+         }
+      } else {
+         g_debug("%s: DND data is not ready, Could be a drag leave.\n",
+                 __FUNCTION__);
+      }
+   }
+}
+
+
+/*
+ * END GTK+ Callbacks (dndcp as drag destination: guest-to-host)
+ ****************************************************************************
+ */
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::SetCPClipboardFromGtk_File --
+ *
+ *      Extract file list from GTK4.
+ *      And construct cross-platform clipboard.
+ *
+ * Results:
+ *      Returns true if conversion succeeded, false otherwise.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool DnDUIX11::SetCPClipboardFromGtk_File(utf::string &source)
+{
+   DnDFileList fileList;
+   DynBuf buf;
+   size_t index = 0;
+   char *newPath;
+   size_t newPathLen;
+   uint64 totalSize;
+   int64 size;
+
+   g_debug("%s: Got file list: [%s]\n", __FUNCTION__, source.c_str());
+
+   if (source.length() == 0) {
+      g_debug("%s: empty file list!\n", __FUNCTION__);
+      return false;
+   }
+
+   /*
+    * In gnome, before file list there may be a extra line indicating it
+    * is a copy or cut.
+    */
+   if (source.length() >= 5 && source.compare(0, 5, "copy\n") == 0) {
+      source = source.erase(0, 5);
+   }
+
+   if (source.length() >= 4 && source.compare(0, 4, "cut\n") == 0) {
+      source = source.erase(0, 4);
+   }
+
+   while (source.length() > 0 &&
+          (source[0] == '\n' || source[0] == '\r' || source[0] == ' ')) {
+      source = source.erase(0, 1);
+   }
+
+   while ((newPath = DnD_UriListGetNextFile(source.c_str(),
+                                            &index,
+                                            &newPathLen)) != NULL) {
+      char *newRelPath;
+
+      /*TODO: refactor compiler option segments and merge them together*/
+#if defined(__linux__)
+      if (DnD_UriIsNonFileSchemes(newPath)) {
+         /* Try to get local file path for non file uri. */
+         GFile *file = g_file_new_for_uri(newPath);
+         free(newPath);
+         if (!file) {
+            g_debug("%s: g_file_new_for_uri failed\n", __FUNCTION__);
+            return false;
+         }
+         newPath = g_file_get_path(file);
+         g_object_unref(file);
+         if (!newPath) {
+            g_debug("%s: g_file_get_path failed\n", __FUNCTION__);
+            return false;
+         }
+      }
+#endif
+      /*
+       * Parse relative path.
+       */
+      newRelPath = Str_Strrchr(newPath, DIRSEPC) + 1; // Point to char after '/'
+
+      /* Keep track of how big the dnd files are. */
+      if ((size = File_GetSizeEx(newPath)) >= 0) {
+         totalSize += size;
+      } else {
+         g_debug("%s: unable to get file size for %s\n", __FUNCTION__, newPath);
+      }
+      g_debug("%s: Adding newPath '%s' newRelPath '%s'\n", __FUNCTION__,
+              newPath, newRelPath);
+      fileList.AddFile(newPath, newRelPath);
+#if defined(__linux__)
+      char *newUri = HgfsUri_ConvertFromPathToHgfsUri(newPath, false);
+      fileList.AddFileUri(newUri);
+      free(newUri);
+#endif
+      free(newPath);
+   }
+
+   DynBuf_Init(&buf);
+   fileList.SetFileSize(totalSize);
+   if (fileList.ToCPClipboard(&buf, false)) {
+      CPClipboard_SetItem(&mClipboard, CPFORMAT_FILELIST, DynBuf_Get(&buf),
+                          DynBuf_GetSize(&buf));
+   }
+   DynBuf_Destroy(&buf);
+#if defined(__linux__)
+   if (fileList.ToUriClipboard(&buf)) {
+      CPClipboard_SetItem(&mClipboard, CPFORMAT_FILELIST_URI, DynBuf_Get(&buf),
+                          DynBuf_GetSize(&buf));
+   }
+   DynBuf_Destroy(&buf);
+#endif
+   return true;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::SetCPClipboardFromGtk_PlainText --
+ *
+ *      Extract text from GTK4. And construct cross-platform clipboard.
+ *
+ * Results:
+ *      Returns true if conversion succeeded, false otherwise.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool DnDUIX11::SetCPClipboardFromGtk_PlainText(utf::string &source)
+{
+   if (source.size() > 0 &&
+       source.size() < DNDMSG_MAX_ARGSZ &&
+       CPClipboard_SetItem(&mClipboard, CPFORMAT_TEXT, source.c_str(), source.size() + 1)) {
+      g_debug("%s: Got text, size %" FMTSZ "u\n", __FUNCTION__, source.size());
+   } else {
+      g_warning("%s: Failed to get text\n", __FUNCTION__);
+      return false;
+   }
+   return true;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::SetCPClipboardFromGtk_RTF --
+ *
+ *      Extract RTF from GTK4. And construct cross-platform clipboard.
+ *
+ * Results:
+ *      Returns true if conversion succeeded, false otherwise.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool DnDUIX11::SetCPClipboardFromGtk_RTF(utf::string &source)
+{
+   if (source.size() > 0 &&
+       source.size() < DNDMSG_MAX_ARGSZ &&
+       CPClipboard_SetItem(&mClipboard, CPFORMAT_RTF, source.c_str(), source.size() + 1)) {
+      g_debug("%s: Got RTF, size %" FMTSZ "u\n", __FUNCTION__, source.size());
+      return true;
+   } else {
+      g_warning("%s: Failed to get RTF\n", __FUNCTION__);
+   }
+
+   return false;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::SetCPClipboardFromGtk --
+ *
+ *      Construct cross-platform clipboard from GTK+ selection_data.
+ *
+ * Results:
+ *      Returns true if conversion succeeded, false otherwise.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+DnDUIX11::SetCPClipboardFromGtk(std::string &data, std::string &drop_type) // IN
+{
+   DnDFileList fileList;
+   DynBuf buf;
+   uint64 totalSize = 0;
+   int64 size;
+   bool ret = false;
+
+   utf::string source = utf::string(data);
+   utf::string type = utf::string(drop_type);
+
+   /* Try to get file list. */
+   if (mDnD->CheckCapability(DND_CP_CAP_FILE_DND) && type == DRAG_TARGET_NAME_URI_LIST) {
+      ret = SetCPClipboardFromGtk_File(source);
+   }
+   /* Try to get plain text. */
+   if (mDnD->CheckCapability(DND_CP_CAP_PLAIN_TEXT_DND) && TargetIsPlainText(type)) {
+      ret = SetCPClipboardFromGtk_PlainText(source);
+   }
+   /* Try to get RTF string. */
+   if (mDnD->CheckCapability(DND_CP_CAP_RTF_DND) && TargetIsRichText(type)) {
+      ret = SetCPClipboardFromGtk_RTF(source);
+   }
+
+   return ret;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::GetLastDirName --
+ *
+ *      Try to get last directory name from a full path name.
+ *
+ *      What this really means is to get the basename of the parent's directory
+ *      name, intended to isolate an individual DND operation's staging directory
+ *      name.
+ *
+ *         E.g. /tmp/VMwareDnD/abcd137 â†’ abcd137
+ *
+ * Results:
+ *      Returns session directory name on success, empty string otherwise.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+utf::string
+DnDUIX11::GetLastDirName(const utf::string &str)
+{
+   char *baseName;
+   utf::string stripSlash;
+   char *path = File_StripSlashes(str.c_str());
+   if (path) {
+      stripSlash = utf::CopyAndFree(path);
+   }
+
+   File_GetPathName(stripSlash.c_str(), NULL, &baseName);
+   if (baseName) {
+      return utf::CopyAndFree(baseName);
+   } else {
+      return utf::string();
+   }
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::GetNextPath --
+ *
+ *      Convoluted somethingerother.
+ *
+ *      XXX Something here involves URI parsing and encoding.  Get to the bottom
+ *      of this and use shared URI code.
+ *
+ *      Original description:
+ *         Provide a substring containing the next path from the provided
+ *         NUL-delimited string starting at the provided index.
+ *
+ * Results:
+ *      Returns "a string with the next path or empty string if there are no
+ *      more paths".
+ *
+ * Side effects:
+ *      Updates index.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+utf::utf8string
+DnDUIX11::GetNextPath(utf::utf8string& str,     // IN: NUL-delimited path list
+                      size_t& index)            // IN/OUT: index into string
+{
+   utf::utf8string ret;
+   size_t start;
+
+   if (index >= str.length()) {
+      return "";
+   }
+
+   for (start = index; str[index] != '\0' && index < str.length(); index++) {
+      /*
+       * Escape reserved characters according to RFC 1630.  We'd use
+       * Escape_Do() if this wasn't a utf::string, but let's use the same table
+       * replacement approach.
+       */
+      static char const Dec2Hex[] = {
+         '0', '1', '2', '3', '4', '5', '6', '7',
+         '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
+      };
+
+      unsigned char ubyte = str[index];
+
+      if (ubyte == '#' ||   /* Fragment identifier delimiter */
+          ubyte == '?' ||   /* Query string delimiter */
+          ubyte == '*' ||   /* "Special significance within specific schemes" */
+          ubyte == '!' ||   /* "Special significance within specific schemes" */
+          ubyte == '%' ||   /* Escape character */
+          ubyte >= 0x80) {  /* UTF-8 encoding bytes */
+         str.replace(index, 1, "%");
+         str.insert(index + 1, 1, Dec2Hex[ubyte >> 4]);
+         str.insert(index + 2, 1, Dec2Hex[ubyte & 0xF]);
+         index += 2;
+      }
+   }
+
+   ret = str.substr(start, index - start);
+   g_debug("%s: nextpath: %s", __FUNCTION__, ret.c_str());
+   index++;
+   return ret;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::TargetDragEnter --
+ *
+ *      With the source's drag selection data on the clipboard, signal to
+ *      host to begin guest-to-host drag-and-drop.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::TargetDragEnter()
+{
+   TRACE_CALL();
+
+   /* Check if there is valid data with current detection window. */
+   if (!CPClipboard_IsEmpty(&mClipboard)) {
+      g_debug("%s: got valid data from detWnd.\n", __FUNCTION__);
+      mDnD->DestUIDragEnter(&mClipboard);
+   }
+
+   /*
+    * Show the window, and position it under the current mouse position.
+    * This is particularly important for KDE 3.5 guests.
+    */
+   mDetWnd->SimulateXEvents(true, false, true, true, false, 0, 0);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::SourceDragStartDone --
+ *
+ *      Tell host that we're done with host-to-guest drag-and-drop
+ *      initialization.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::SourceDragStartDone()
+{
+   TRACE_CALL();
+   mInHGDrag = true;
+   mDnD->SrcUIDragBeginDone();
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::SetBlockControl --
+ *
+ *      Set block control member.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::SetBlockControl(DnDBlockControl *blockCtrl)   // IN
+{
+   mBlockCtrl = blockCtrl;
+}
+
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::SourceUpdateFeedback --
+ *
+ *      Forward feedback from our drop source to the host.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::SourceUpdateFeedback(DND_DROPEFFECT effect) // IN: feedback to send to the UI-independent DnD layer.
+{
+   TRACE_CALL();
+   mDnD->SrcUIUpdateFeedback(effect);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::GetTimeInMillis --
+ *
+ *      Get Unix time in milliseconds.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+unsigned long
+DnDUIX11::GetTimeInMillis()
+{
+   VmTimeType atime;
+
+   Hostinfo_GetTimeOfDay(&atime);
+   return((unsigned long)(atime / 1000));
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::VmXDnDVersionChanged --
+ *
+ *      Update version information in mDnD.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::VmxDnDVersionChanged(RpcChannel *chan,        // IN
+                               uint32 version)          // IN
+{
+   ASSERT(mDnD);
+   mDnD->VmxDnDVersionChanged(version);
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DnDUIX11::UpdateWorkArea --
+ *
+ *      Updates mOrigin in response to changes to _NET_workArea.
+ *
+ * Results:
+ *      None.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+void
+DnDUIX11::UpdateWorkArea()
+{
+   TRACE_CALL();
+
+   std::vector<unsigned long> values;
+   if (xutils::GetCardinalList(mGdkSurface, "_NET_WORKAREA", values)
+       && values.size() > 0
+       && values.size() % 4 == 0) {
+      /*
+       * wm-spec: _NET_WORKAREA, x, y, width, height CARDINAL[][4]/32
+       *
+       * For the purposes of drag-and-drop, we're okay with using the screen-
+       * agnostic _NET_WORKAREA atom, as the guest VM really deals with only
+       * one logical monitor.
+       */
+      unsigned long desktop = 0;
+      xutils::GetCardinal(mGdkSurface, "_NET_CURRENT_DESKTOP", desktop);
+
+      mOriginX = values[0 + 4 * desktop];
+      mOriginY = values[1 + 4 * desktop];
+   } else {
+      mOriginX = 0;
+      mOriginY = 0;
+   }
+
+   g_debug("%s: new origin at (%d, %d)\n", __FUNCTION__, mOriginX, mOriginY);
+}
diff --git a/open-vm-tools/services/plugins/dndcp/dragDetWndX11GTK4.cpp b/open-vm-tools/services/plugins/dndcp/dragDetWndX11GTK4.cpp
new file mode 100644 (file)
index 0000000..9bda763
--- /dev/null
@@ -0,0 +1,1069 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+/**
+ * @file dragDetWndX11GTK4.cpp
+ *
+ * Detection window code for Linux/X11, based on Gtkmm-4.0.
+ */
+
+#define G_LOG_DOMAIN "dndcp"
+
+#include "tracer.hh"
+#include "dragDetWndX11GTK4.h"
+#include "dndUIX11GTK4.h"
+#include <list>
+
+extern "C" {
+#include <X11/extensions/XTest.h>       /* for XTest*() */
+#include <gtk/gtk.h>
+#include <gdk/x11/gdkx.h>
+#include <X11/Xatom.h>
+}
+
+
+/**
+ *
+ * Constructor.
+ */
+
+DragDetWnd::DragDetWnd(int UseUInput_fd)
+   : m_isVisible(false),
+     mWnd(NULL),
+     mSource(NULL),
+     mTarget(NULL),
+     mUseUInput(false),
+     mScreenWidth(0),
+     mScreenHeight(0)
+{
+   std::vector<GType> types;
+   mWnd = new Gtk::Window();
+   if (!mWnd) {
+      g_error("%s: unable to allocate DragDetWnd object\n", __FUNCTION__);
+      return;
+   }
+
+   // Makes this window can grab focus.
+   mWnd->set_decorated(false);
+   mWnd->set_resizable(false);
+   mWnd->set_modal(false);
+   mWnd->set_can_focus(true);
+   mWnd->set_focusable(true);
+   mWnd->set_sensitive(false);
+   // Makes this window transparent because we don't want user to see it.
+   mWnd->set_opacity(0.01);
+   mWnd->set_visible(true);
+   mWnd->set_visible(false);
+
+   //dnd Source controller
+   mSource = Gtk::DragSource::create();
+   if (!mSource) {
+      g_error("%s: unable to allocate source for DragDetWnd object\n",
+              __FUNCTION__);
+      goto fail;
+   }
+   mSource->set_actions(Gdk::DragAction::COPY | Gdk::DragAction::MOVE);
+
+   //dnd target controller
+   mTarget = Gtk::DropTarget::create(G_TYPE_INVALID, Gdk::DragAction::COPY | Gdk::DragAction::MOVE);
+   if (!mTarget) {
+      g_error("%s: unable to allocate target for DragDetWnd object\n",
+              __FUNCTION__);
+      goto fail;
+   }
+
+   // Put GDK_TYPE_FILE_LIST at first, and that will make GTK report file list when possible.
+   types.push_back(GDK_TYPE_FILE_LIST);
+   types.push_back(G_TYPE_FILE);
+   types.push_back(G_TYPE_STRING);
+   mTarget->set_gtypes(types);
+   // Let GTK preload data early.
+   mTarget->set_preload(true);
+
+   // Uinput simulation for wayland backend.
+   if (UseUInput_fd != -1) {
+       g_debug("%s: Using uinput simulation", __FUNCTION__);
+       Display *display = XOpenDisplay(NULL);
+       Screen * scrn = DefaultScreenOfDisplay(display);
+       if (FakeMouse_Init(UseUInput_fd, scrn->width, scrn->height)) {
+           mUseUInput = true;
+           mScreenWidth = scrn->width;
+           mScreenHeight = scrn->height;
+           g_debug("%s: mScreenWidth is %d, mScreenHeight is %d", __FUNCTION__, mScreenWidth, mScreenHeight);
+       }
+       // Disconnect the connection to X Server.
+       XCloseDisplay(display);
+   }
+   return;
+fail:
+   if (mWnd) {
+       delete mWnd;
+       mWnd = NULL;
+   }
+}
+
+
+/**
+ *
+ * Destructor.
+ */
+
+DragDetWnd::~DragDetWnd()
+{
+   if (mUseUInput) {
+      FakeMouse_Destory();
+   }
+   if (mWnd) {
+      delete mWnd;
+      mWnd = NULL;
+   }
+}
+
+
+/**
+ * Get the actual widget.
+ */
+
+Gtk::Widget *
+DragDetWnd::GetWnd()
+{
+   return static_cast<Gtk::Widget*>(mWnd);
+}
+
+/**
+ * Register GTK callback which was triggered when a drag begin happens.
+ */
+
+bool
+DragDetWnd::register_source_drag_begin_handler(source_drag_begin_handler handler, void *ctx)
+{
+   mSource->signal_drag_begin().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback which was triggered when a drag end happens.
+ */
+
+bool
+DragDetWnd::register_source_drag_end_handler(source_drag_end_handler handler, void *ctx)
+{
+   mSource->signal_drag_end().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback when a drag has failed
+ */
+
+bool
+DragDetWnd::register_source_drag_cancel_handler(source_drag_cancel_handler handler, void *ctx)
+{
+   mSource->signal_drag_cancel().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback when a drag is about to be initiated.
+ */
+
+bool
+DragDetWnd::register_source_drag_prepare_handler(source_drag_prepare_handler handler, void *ctx)
+{
+   mSource->signal_prepare().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback when a drop operation is about to begin.
+ */
+
+bool
+DragDetWnd::register_target_drag_accept_handler(target_drag_accept_handler handler, void *ctx)
+{
+   mTarget->signal_accept().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback when the pointer enters the widget.
+ */
+
+bool
+DragDetWnd::register_target_drag_enter_handler(target_drag_enter_handler handler, void *ctx)
+{
+   mTarget->signal_enter().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback when the user drops the data onto the widget.
+ */
+
+bool
+DragDetWnd::register_target_drag_drop_handler(target_drag_drop_handler handler, void *ctx)
+{
+   mTarget->signal_drop().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback when the pointer leaves the widget.
+ */
+
+bool
+DragDetWnd::register_target_drag_leave_handler(target_drag_leave_handler handler, void *ctx)
+{
+   mTarget->signal_leave().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Register GTK callback when the drop value changed. when set droptarget controller preload,
+ * the value may be ready befor drop action happen. When drop leave happens, the value will be
+ * set to NULL.
+ */
+
+bool
+DragDetWnd::register_target_value_handler(target_drag_value_handler handler, void *ctx)
+{
+    mTarget->property_value().signal_changed().connect(sigc::bind(sigc::ptr_fun(handler), ctx));
+    return true;
+}
+
+
+/**
+ * Register GTK callback while the pointer is moving over the drop target.
+ */
+
+bool
+DragDetWnd::register_target_drag_motion_handler(target_drag_motion_handler handler, void *ctx)
+{
+   mTarget->signal_motion().connect(sigc::bind(handler, ctx), false);
+   return true;
+}
+
+
+/**
+ * Enable drag source and target.
+ */
+
+bool
+DragDetWnd::EnableDND(void)
+{
+   g_debug("%s\n", __FUNCTION__);
+   mWnd->add_controller(mSource);
+   mWnd->add_controller(mTarget);
+   return true;
+}
+
+
+/**
+ * Get the current drag contax from source.
+ */
+
+unsigned long
+DragDetWnd::Get_Current_Drag_ctx(void)
+{
+   Glib::RefPtr< Gdk::Drag > current_drag = mSource->get_drag();
+   if (current_drag != NULL)
+       return (unsigned long) current_drag->gobj();
+   else
+       return 0;
+}
+
+
+/**
+ * Disable drag source and target.
+ */
+
+bool
+DragDetWnd::DisableDND(void)
+{
+   g_debug("%s\n", __FUNCTION__);
+   mWnd->remove_controller(mSource);
+   mWnd->remove_controller(mTarget);
+   return true;
+}
+
+
+/**
+ * Query if drop value is ready.
+ */
+
+bool
+DragDetWnd::Is_DropValue_Ready(void)
+{
+    Glib::ValueBase value= mTarget->get_value();
+    return G_IS_VALUE(value.gobj());
+}
+
+
+/**
+ * Query if current drop type is supported.
+ */
+
+bool
+DragDetWnd::Is_Current_Drop_Supported(void)
+{
+   Glib::RefPtr< Gdk::Drop > current_drop = mTarget->get_drop();
+   return this->Is_Drop_Supported(current_drop);
+}
+
+
+/**
+ * Query if a drop type is supported.
+ */
+
+bool
+DragDetWnd::Is_Drop_Supported(const Glib::RefPtr< Gdk::Drop > &drop)
+{
+   Glib::RefPtr<Gdk::ContentFormats> supported_format = drop->get_formats();
+   g_debug("%s content type is %s", __FUNCTION__, supported_format->to_string().c_str());
+
+   return (supported_format->contain_gtype(GDK_TYPE_FILE_LIST) ||
+           supported_format->contain_gtype(G_TYPE_STRING));
+}
+
+
+/**
+ * Query the current drop context from target.
+ */
+
+unsigned long
+DragDetWnd::Get_Current_Drop_ctx(void)
+{
+   Glib::RefPtr< Gdk::Drop > current_drop = mTarget->get_drop();
+   if (current_drop != NULL)
+       return (unsigned long) mTarget->get_drop()->gobj();
+   else
+       return 0;
+}
+
+
+/**
+ * Get drop value from detect window.
+ */
+
+bool
+DragDetWnd::Get_Drop_Value(std::string &value, std::string &value_type)
+{
+    Glib::ValueBase cur_value= mTarget->get_value();
+    bool ret = false;
+
+    if (G_IS_VALUE(cur_value.gobj())) {
+      if (G_VALUE_HOLDS(cur_value.gobj(), GDK_TYPE_FILE_LIST)) {
+         GSList *file_list = gdk_file_list_get_files(static_cast<GdkFileList *>(g_value_get_boxed(cur_value.gobj())));
+         g_debug("%s Get file list ...", __FUNCTION__);
+         for (GSList *l = file_list; l != nullptr; l = l->next) {
+            GFile *file = static_cast<GFile *>(l->data);
+            gchar *uri = g_file_get_uri(file);
+            if (uri) {
+               g_debug("%s Get file uri: %s", __FUNCTION__, uri);
+               value += uri;
+               value += '\n';
+            }
+         }
+         if (value.length()) {
+            ret = true;
+            value_type = DRAG_TARGET_NAME_URI_LIST;
+         }
+      } else if (G_VALUE_HOLDS(cur_value.gobj(), G_TYPE_FILE)) {
+         GFile* gfile = G_FILE(g_value_get_object(cur_value.gobj()));
+         g_debug("%s Get file ...", __FUNCTION__);
+         if (gfile) {
+            gchar *uri = g_file_get_uri(gfile);
+             if (uri) {
+               g_debug("%s Get file uri: %s", __FUNCTION__, uri);
+               value += uri;
+            }
+        }
+      } else if (G_VALUE_HOLDS(cur_value.gobj(), G_TYPE_STRING)) {
+         const gchar* text= g_value_get_string(cur_value.gobj());
+         Glib::RefPtr<Gdk::ContentFormats> supported_format = mTarget->get_drop()->get_formats();
+
+         value += text;
+         g_debug("%s Get str: %s", __FUNCTION__, text);
+         if (supported_format->to_string().find(TARGET_NAME_TEXT_RTF) != Glib::ustring::npos) {
+             value_type = TARGET_NAME_TEXT_RTF;
+         } else {
+             value_type = TARGET_NAME_STRING;
+         }
+         g_debug("%s Set type as: %s", __FUNCTION__, value_type.c_str());
+         ret = true;
+
+      } else {
+         g_warning("%s Unsupported DND Value type %s",
+                  __FUNCTION__, g_type_name(G_TYPE_FROM_INSTANCE(cur_value.gobj())));
+      }
+    } else
+       g_debug("%s No value available %ld", __FUNCTION__, (unsigned long) cur_value.gobj());
+
+   return ret;
+}
+
+
+/**
+ * Flush the X connection.
+ */
+
+void
+DragDetWnd::Flush()
+{
+   Glib::RefPtr<Gdk::Display> gdkdisplay = Gdk::Display::get_default();
+   g_debug("%s\n", __FUNCTION__);
+   if (gdkdisplay) {
+      gdkdisplay->sync();
+      gdkdisplay->flush();
+   }
+}
+
+
+/**
+ * Show the window.
+ */
+
+void
+DragDetWnd::Show(void)
+{
+   Glib::RefPtr<Gdk::Display> display = Gdk::Display::get_default();
+   int width = 0;
+   int height = 0;
+   g_debug("%s: enter\n", __FUNCTION__);
+   if (display) {
+      Glib::RefPtr<Gio::ListModel> monitors = display->get_monitors();
+      if (!monitors || monitors->get_n_items() == 0)
+         return;
+
+      GListModel* c_model = monitors->gobj();
+      for (guint i = 0; i < monitors->get_n_items(); ++i) {
+         GObject* c_item = G_OBJECT(g_list_model_get_item(c_model, i));
+         Glib::RefPtr<Gdk::Monitor> monitor = Glib::wrap(GDK_MONITOR(c_item), true);
+         Gdk::Rectangle geometry = monitor->property_geometry();
+         mWnd->set_default_size(geometry.get_width(), geometry.get_height());
+         break;
+      }
+   } else {
+          g_error("%s: Cannot get default display \n", __FUNCTION__);
+          return;
+   }
+
+   mWnd->set_sensitive(true);
+   mWnd->set_visible(true);
+
+   if (mWnd->grab_focus()) {
+      g_debug("%s: grab_focus successfully\n", __FUNCTION__);
+   } else {
+      Warning("%s: grab_focus failed! May unable to detect drag actions\n", __FUNCTION__);
+   }
+
+   m_isVisible = true;
+   // Add a delay to let dnd warming up
+   usleep(300);
+
+   if (mWnd->get_focus())
+   {
+      g_debug("%s: detwnd have focus on %s\n", __FUNCTION__, G_OBJECT_TYPE_NAME(mWnd->get_focus()->gobj()));
+   }
+   else
+      g_debug("%s: detwnd doesn't have focus\n", __FUNCTION__);
+
+   g_debug("%s: exiting \n", __FUNCTION__);
+}
+
+
+/**
+ * Hide the window.
+ */
+
+void
+DragDetWnd::Hide(void)
+{
+   g_debug("%s: enter\n", __FUNCTION__);
+   g_debug("%s: detwnd current visible is  %d\n", __FUNCTION__, mWnd->is_visible());
+   g_debug("%s: detwnd current focus is %d", __FUNCTION__, gtk_widget_has_focus(GTK_WIDGET(mWnd->gobj())));
+
+   mWnd->set_sensitive(false);
+   mWnd->set_visible(false);
+   m_isVisible = false;
+   g_debug("%s: exiting \n", __FUNCTION__);
+}
+
+
+/**
+ * Lower the the GTK window by set lower its surface
+ */
+
+static bool lower_wnd(Gtk::Window *window)
+{
+   GtkWidget* widget = GTK_WIDGET(window->gobj());
+   GtkNative* native = gtk_widget_get_native(widget);
+
+   if (native) {
+      GdkSurface* surface = gtk_native_get_surface(native);
+      if (surface && GDK_IS_TOPLEVEL(surface)) {
+         gdk_toplevel_lower(GDK_TOPLEVEL(surface));
+         return true;
+      }
+   }
+
+   return false;
+}
+
+
+/**
+ * Get the monitor of the GTK window
+ */
+
+static Glib::RefPtr<Gdk::Monitor> get_monitor(Gtk::Window *window)
+{
+   GtkWidget* widget = GTK_WIDGET(window->gobj());
+   GtkNative* native = gtk_widget_get_native(widget);
+
+   if (native) {
+      GdkSurface* surface = gtk_native_get_surface(native);
+      g_object_ref(surface);
+      Glib::RefPtr<Gdk::Surface> share_surface(Glib::wrap(surface));
+      if (surface)
+         return Gdk::Display::get_default()->get_monitor_at_surface(share_surface);
+   }
+   return NULL;
+}
+
+
+/**
+ * Raise the window.
+ */
+
+void
+DragDetWnd::Raise(void)
+{
+   mWnd->present();
+   g_debug("%s: detwnd Presenting\n", __FUNCTION__);
+   Flush();
+}
+
+
+/**
+ * Lower the window.
+ */
+
+void
+DragDetWnd::Lower(void)
+{
+   lower_wnd(mWnd);
+   Flush();
+}
+
+
+/**
+ *
+ * Get the width of the screen associated with this window.
+ *
+ * @return width of screen, in pixels.
+ */
+
+int
+DragDetWnd::GetScreenWidth(void)
+{
+   Gdk::Rectangle geometry;
+   Glib::RefPtr<Gdk::Monitor> monitor = get_monitor(mWnd);
+   monitor->get_geometry(geometry);
+   return geometry.get_width();
+}
+
+
+/**
+ *
+ * Get the height of the screen associated with this window.
+ *
+ * @return height of screen, in pixels.
+ */
+
+int
+DragDetWnd::GetScreenHeight(void)
+{
+   Gdk::Rectangle geometry;
+   Glib::RefPtr<Gdk::Monitor> monitor = get_monitor(mWnd);
+   monitor->get_geometry(geometry);
+   return geometry.get_height();
+}
+
+
+/**
+ *
+ * Set the geometry of the window.
+ *
+ * @param[in] x desired x-coordinate of the window.
+ * @param[in] y desired y-coordinate of the window.
+ * @param[in] width desired width of the window.
+ * @param[in] height desired height of the window.
+ */
+
+void
+DragDetWnd::SetGeometry(const int x,
+                        const int y,
+                        const int width,
+                        const int height)
+{
+   GtkWidget* widget = GetWnd()->gobj();
+   GtkNative* native = gtk_widget_get_native(widget);
+   Gtk::Window* gdkwin = dynamic_cast<Gtk::Window*>(GetWnd());
+   GdkSurface* surface = gtk_native_get_surface(native);
+   g_debug("%s: set Geo at (%d, %d, %d, %d)\n", __FUNCTION__, x, y, width, height);
+
+   if (gdkwin && surface) {
+      gdkwin->set_default_size(width, height);
+      if (GDK_IS_X11_SURFACE(surface)) {
+         Display* display = gdk_x11_display_get_xdisplay(gdk_surface_get_display(surface));
+         Window xwindow = gdk_x11_surface_get_xid(surface);
+         XMoveWindow(display, xwindow, x, y);
+         XFlush(display);
+      }
+      Flush();
+   }
+}
+
+
+/**
+ *
+ * Get the current geometry of the window.
+ *
+ * @param[out] x current x-coordinate of the window.
+ * @param[out] y current y-coordinate of the window.
+ * @param[out] width current width of the window.
+ * @param[out] height current height of the window.
+ *
+ * @note The current geometry may be inaccurate if retrieved too quickly
+ * after a change made by SetGeometry(). This is due to the realities of
+ * X and window managers. Some of this is mitigated by the use of flush()
+ * and sync() calls in SetGeometry(), but these are no guarantee.
+ */
+
+void
+DragDetWnd::GetGeometry(int &x, int &y, int &width, int &height)
+{
+   GtkWidget* widget = GetWnd()->gobj();
+   GtkNative* native = gtk_widget_get_native(widget);
+   GdkSurface* surface = gtk_native_get_surface(native);
+
+   width = gdk_surface_get_width(surface);
+   height = gdk_surface_get_height(surface);
+
+   if (GDK_IS_X11_SURFACE(surface)) {
+      Display* display = gdk_x11_display_get_xdisplay(gdk_surface_get_display(surface));
+      Window xwindow = gdk_x11_surface_get_xid(GDK_X11_SURFACE(surface));
+
+      Window root, child;
+      int dx, dy;
+      unsigned int w, h, bw, depth;
+
+      if (XGetGeometry(display, xwindow, &root, &dx, &dy, &w, &h, &bw, &depth)) {
+         XTranslateCoordinates(display, xwindow, root, 0, 0, &x, &y, &child);
+      }
+   }
+}
+
+
+/**
+ *
+ *  Show or hide detect window and move it to the positon.
+ *
+ */
+
+void
+DragDetWnd::UpdateDetWnd(bool show, int32 x, int32 y)
+{
+  g_debug("%s: enter 0x%lx show %d x %d y %d\n",
+         __FUNCTION__,
+         (unsigned long) Get_Current_Drop_ctx(), show, x, y);
+
+   /* If the window is being shown, move it to the right place. */
+   if (show) {
+      Show();
+      Raise();
+      SetGeometry(x, y, DRAG_DET_WINDOW_WIDTH * 2, DRAG_DET_WINDOW_WIDTH * 2);
+      g_debug("%s: show at (%d, %d, %d, %d)\n", __FUNCTION__, x, y, DRAG_DET_WINDOW_WIDTH * 2, DRAG_DET_WINDOW_WIDTH * 2);
+      /*
+       * Wiggle the mouse here. Especially for G->H DnD, this improves
+       * reliability of making the drag escape the guest window immensly.
+       * Stolen from the legacy V2 DnD code.
+       */
+
+      SimulateMouseMove(x + 2, y + 2);
+      SetIsVisible(true);
+   } else {
+      g_debug("%s: hide\n", __FUNCTION__);
+      Hide();
+      SetIsVisible(false);
+   }
+}
+
+
+/**
+ *  Start drag from drag source with current coordinate.
+ */
+
+void
+DragDetWnd::StartDrag(Glib::RefPtr<Gdk::ContentProvider> provider,
+                      int mouseX,
+                      int mouseY)
+{
+   g_debug("%s: start drag from offset (%d, %d)\n", __FUNCTION__,
+           mouseX, mouseY);
+   mSource->set_content(provider);
+}
+
+
+/**
+ *  adjust the current x,y coordinate slightly.
+ */
+
+static
+bool adjust_coord(int *x, int *y, int width, int height)
+{
+   bool change = false;
+   /*
+    * first do left and top edges.
+    */
+   if (*x <= 5){
+      *x = 6;
+      change = true;
+   }
+
+   if (*y <= 5){
+      *y = 6;
+      change = true;
+   }
+
+   /*
+    * next, move result away from right and bottom edges.
+    */
+   if (*x > width - 5){
+      *x = width - 6;
+      change = true;
+   }
+
+   if (*y > height - 5){
+      *y = height - 6;
+      change = true;
+   }
+   return change;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * TryXTestFakeDeviceButtonEvent --
+ *
+ *      Fake X mouse events in device level.
+ *
+ *      XXX The function will only be called if XTestFakeButtonEvent does
+ *      not work for mouse button release. Later on we may only call this
+ *      one for mouse button simulation if this is more reliable.
+ *
+ * Results:
+ *      Returns true on success, false on failure.
+ *
+ * Side effects:
+ *      Generates mouse events.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+static
+bool TryXTestSimulateDeviceButtonEvent(GdkSurface* surface)
+{
+   XDeviceInfo *list = NULL;
+   XDeviceInfo *list2 = NULL;
+   XDevice *tdev = NULL;
+   XDevice *buttonDevice = NULL;
+   int numDevices = 0;
+   int i = 0;
+   int j = 0;
+   XInputClassInfo *ip = NULL;
+   Display *dndXDisplay;
+
+   if (!surface) {
+      g_debug("%s: unable to get widget\n", __FUNCTION__);
+      return false;
+   }
+
+   dndXDisplay = GDK_SURFACE_XDISPLAY(surface);
+
+   /* First get list of all input device. */
+   if (!(list = XListInputDevices (dndXDisplay, &numDevices))) {
+      g_debug("%s: XListInputDevices failed\n", __FUNCTION__);
+      return false;
+   } else {
+      g_debug("%s: XListInputDevices got %d devices\n", __FUNCTION__, numDevices);
+   }
+
+   list2 = list;
+
+   for (i = 0; i < numDevices; i++, list++) {
+      /* We only care about mouse device. */
+      if (list->use != IsXExtensionPointer) {
+         continue;
+      }
+
+      tdev = XOpenDevice(dndXDisplay, list->id);
+      if (!tdev) {
+         g_debug("%s: XOpenDevice failed\n", __FUNCTION__);
+         continue;
+      }
+
+      for (ip = tdev->classes, j = 0; j < tdev->num_classes; j++, ip++) {
+         if (ip->input_class == ButtonClass) {
+            buttonDevice = tdev;
+            break;
+         }
+      }
+
+      if (buttonDevice) {
+         g_debug("%s: calling XTestFakeDeviceButtonEvent for %s\n",
+               __FUNCTION__, list->name);
+         XTestFakeDeviceButtonEvent(dndXDisplay, buttonDevice, 1, False,
+                                    NULL, 0, CurrentTime);
+         buttonDevice = NULL;
+      }
+      XCloseDevice(dndXDisplay, tdev);
+   }
+   XFreeDeviceList(list2);
+   return true;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DragDetWnd::SimulateXEvents --
+ *
+ *      Fake X mouse events and window movement for the detection window.
+ *
+ *      This function shows the detection window and generates button
+ *      press/release and pointer motion events.
+ *
+ *      XXX This code should be implemented using GDK APIs.
+ *          (gdk_display_warp_pointer?)
+ *
+ *
+ * Results:
+ *      Returns true if generated X events, false on failure.
+ *
+ * Side effects:
+ *      A ton of things.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+DragDetWnd::SimulateXEvents(const bool showWidget, const bool buttonEvent,
+                            const bool buttonPress, const bool moveWindow,
+                            const bool coordsProvided,
+                            const int xCoord, const int yCoord)
+{
+   Display *dndXDisplay;
+   Window rootWnd;
+   Window dndXWindow;
+   Window rootReturn;
+   Window childReturn;
+   GdkSurface* surface;
+
+   bool ret = false;
+   int x, y;
+   int rootXReturn, rootYReturn;
+   int winXReturn, winYReturn;
+   unsigned int maskReturn;
+
+   TRACE_CALL();
+
+   x = xCoord;
+   y = yCoord;
+
+   surface = gtk_native_get_surface(mWnd->get_native()->gobj());
+   dndXDisplay = GDK_SURFACE_XDISPLAY(surface);
+   dndXWindow = GDK_SURFACE_XID(surface);
+   rootWnd = RootWindow(dndXDisplay, DefaultScreen(dndXDisplay));
+
+   /*
+    * Turn on X synchronization in order to ensure that our X events occur in
+    * the order called.  In particular, we want the window movement to occur
+    * before the mouse movement so that the events we are coercing do in fact
+    * happen.
+    */
+   XSynchronize(dndXDisplay, True);
+
+   if (showWidget) {
+      g_debug("%s: showing Gtk widget\n", __FUNCTION__);
+      gtk_widget_set_visible(GTK_WIDGET(mWnd->gobj()), true);
+      gtk_window_present(mWnd->gobj());
+
+   }
+
+   /* Get the current location of the mouse if coordinates weren't provided. */
+   if (!coordsProvided) {
+      if (!XQueryPointer(dndXDisplay, rootWnd, &rootReturn, &childReturn,
+                          &rootXReturn, &rootYReturn, &winXReturn, &winYReturn,
+                          &maskReturn)) {
+         Warning("%s: XQueryPointer() returned False.\n", __FUNCTION__);
+         goto exit;
+      }
+
+      g_debug("%s: current mouse is at (%d, %d)\n", __FUNCTION__,
+            rootXReturn, rootYReturn);
+
+      /*
+       * Position away from the edge of the window.
+       */
+      int width = GetScreenWidth();
+      int height = GetScreenHeight();
+
+      x = rootXReturn;
+      y = rootYReturn;
+
+      if (adjust_coord(&x, &y, width, height)) {
+         g_debug("%s: adjusting mouse position. root %d, %d, adjusted %d, %d\n",
+               __FUNCTION__, rootXReturn, rootYReturn, x, y);
+      }
+   }
+
+   if (moveWindow) {
+      /*
+       * Make sure the window is at this point and at the top (raised).  The
+       * window is resized to be a bit larger than we would like to increase
+       * the likelihood that mouse events are attributed to our window -- this
+       * is okay since the window is invisible and hidden on cancels and DnD
+       * finish.
+       */
+      XMoveResizeWindow(dndXDisplay,
+                        dndXWindow,
+                        x - DRAG_DET_WINDOW_WIDTH / 2 ,
+                        y - DRAG_DET_WINDOW_WIDTH / 2,
+                        DRAG_DET_WINDOW_WIDTH,
+                        DRAG_DET_WINDOW_WIDTH);
+      XRaiseWindow(dndXDisplay, dndXWindow);
+      g_debug("%s: move wnd to (%d, %d, %d, %d)\n",
+              __FUNCTION__,
+              x - DRAG_DET_WINDOW_WIDTH / 2 ,
+              y - DRAG_DET_WINDOW_WIDTH / 2,
+              DRAG_DET_WINDOW_WIDTH,
+              DRAG_DET_WINDOW_WIDTH);
+   }
+
+   /*
+    * Generate mouse movements over the window.  The second one makes ungrabs
+    * happen more reliably on KDE, but isn't necessary on GNOME.
+    */
+   if (mUseUInput) {
+       FakeMouse_Move(x, y);
+       FakeMouse_Move(x + 1, y + 1);
+       g_debug("%s: Uinput simulating moving mouse\n", __FUNCTION__);
+   } else {
+       XTestFakeMotionEvent(dndXDisplay, -1, x, y, CurrentTime);
+       XTestFakeMotionEvent(dndXDisplay, -1, x + 1, y + 1, CurrentTime);
+   }
+
+   g_debug("%s: move mouse to (%d, %d) and (%d, %d)\n", __FUNCTION__, x, y, x + 1, y + 1);
+
+   if (buttonEvent)
+   {
+      g_debug("%s: faking left mouse button %s\n", __FUNCTION__,
+              buttonPress ? "press" : "release");
+      if (mUseUInput) {
+         ret = FakeMouse_Click(buttonPress);
+         g_debug("%s: Uinput simulating click mouse with ret %d\n", __FUNCTION__, ret);
+      }
+      else {
+         XTestFakeButtonEvent(dndXDisplay, 1, buttonPress, CurrentTime);
+         XSync(dndXDisplay, False);
+
+         if (!buttonPress) {
+            /*
+             * The button release simulation may be failed with some distributions
+             * like Ubuntu 10.4 and RHEL 6 for guest->host DnD. So first query
+             * mouse button status. If some button is still down, we will try
+             * mouse device level event simulation. For details please refer
+             * to bug 552807.
+             */
+            if (!XQueryPointer(dndXDisplay, rootWnd, &rootReturn, &childReturn,
+                               &rootXReturn, &rootYReturn, &winXReturn,
+                               &winYReturn, &maskReturn)) {
+               Warning("%s: XQueryPointer returned False.\n", __FUNCTION__);
+               goto exit;
+            }
+
+            if ((maskReturn & Button1Mask)
+                 || (maskReturn & Button2Mask)
+                 || (maskReturn & Button3Mask)
+                 || (maskReturn & Button4Mask)
+                 || (maskReturn & Button5Mask)) {
+               Debug("%s: XTestFakeButtonEvent was not working for button "
+                     "release, trying XTestFakeDeviceButtonEvent now.\n",
+                     __FUNCTION__);
+               ret = TryXTestSimulateDeviceButtonEvent(surface);
+            }
+            else {
+               g_debug("%s: XTestFakeButtonEvent was working for button release.\n",
+                       __FUNCTION__);
+               ret = true;
+            }
+         } else {
+            ret = true;
+         }
+      }
+   }
+
+exit:
+   XSynchronize(dndXDisplay, False);
+   return ret;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * DragDetWnd::SimulateMouseMove --
+ *
+ *      Issue a fake mouse move event to the detection window.
+ *
+ * Results:
+ *      Returns true on success, false on failure.
+ *
+ * Side effects:
+ *      Generates mouse events.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+DragDetWnd::SimulateMouseMove(const int x,        // IN: x-coord
+                              const int y)        // IN: y-coord
+{
+   return SimulateXEvents(false, false, false, false, true, x, y);
+}
diff --git a/open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.cc b/open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.cc
new file mode 100644 (file)
index 0000000..853078d
--- /dev/null
@@ -0,0 +1,161 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+#ifndef GTK4
+#error "This should only build with GTK4"
+#endif
+
+#include "xutils/xutilsGTK4.hh"
+
+/* These must be after the gtkmm includes(in xutilsGTK4.hh), as gtkmm is quite picky. */
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+#include <gdk/x11/gdkx.h>
+#include <cstring>
+
+#include "vm_assert.h"
+
+
+namespace xutils {
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * xutils::GetCardinal --
+ *
+ *      Utility function to get only one cardinal from a window property.
+ *
+ * Results:
+ *      true if the function succeeded, along with a value for retValue.
+ *      Otherwise false.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+GetCardinal(Glib::RefPtr<const Gdk::Surface> surface,  // IN: Surface
+            const utf::string& atomName,               // IN: Atom name
+            unsigned long& retValue)                   // OUT: Return value
+{
+   ASSERT(surface);
+   ASSERT(!atomName.empty());
+
+   std::vector<unsigned long> retValues;
+   bool result = GetCardinalList(surface, atomName, retValues);
+
+   if (result && retValues.size() == 1) {
+      retValue = retValues[0];
+      return true;
+   }
+
+   return false;
+}
+
+
+/*
+ *-----------------------------------------------------------------------------
+ *
+ * xutils::GetCardinalList --
+ *
+ *      Utility function to get a cardinal list from a window property.
+ *
+ * Results:
+ *      true if the function succeeded, along with return values for retValues.
+ *      Otherwise false.
+ *
+ * Side effects:
+ *      None.
+ *
+ *-----------------------------------------------------------------------------
+ */
+
+bool
+GetCardinalList(Glib::RefPtr<const Gdk::Surface> surface, // IN: Surface
+                const utf::string& atomName,              // IN: Atom name
+                std::vector<unsigned long>& retValues)    // OUT: Return values
+{
+   ASSERT(surface);
+   ASSERT(surface->get_display());
+   ASSERT(!atomName.empty());
+
+   GdkDisplay* gdkDisplay = const_cast<GdkDisplay*>(surface->get_display()->gobj());
+
+   Atom atom = gdk_x11_get_xatom_by_name_for_display(gdkDisplay, atomName.c_str());
+   Atom type;
+   int format;
+   unsigned long nitems;
+   unsigned long bytes_after;
+   uint8* values;
+   bool result = false;
+
+   gdk_x11_display_error_trap_push(gdkDisplay);
+
+   /*
+    * About the parameter "format" of Xorg XGetWindowProperty function.
+    * The "format" param specifies whether the data should be viewed as a list
+    * of 8-bit, 16-bit, or 32-bit quantities. Possible values are 8, 16, and 32.
+    * This information allows the X server to correctly perform byte-swap
+    * operations as necessary. If the format is 16-bit or 32-bit, you must
+    * explicitly cast your data pointer to an (unsigned char *) in the call
+    * to XChangeProperty.
+    *
+    * For more info, please refer to the document from Xorg:
+    * https://www.x.org/archive/X11R7.5/doc/man/man3/XChangeProperty.3.html
+    */
+   int ret = XGetWindowProperty(GDK_DISPLAY_XDISPLAY(gdkDisplay),
+                                gdk_x11_display_get_xrootwindow(gdkDisplay),
+                                atom, 0, G_MAXLONG, False, XA_CARDINAL, &type,
+                                &format, &nitems, &bytes_after, &values);
+   int err = gdk_x11_display_error_trap_pop(gdkDisplay);
+
+   if (ret == Success && err == 0) {
+      if (type == XA_CARDINAL && nitems > 0) {
+         retValues.resize(nitems);
+         result = true;
+
+         if (format == 8) {
+            for (unsigned long i = 0; i < nitems; i++) {
+               retValues[i] = values[i];
+            }
+         } else if (format == 16) {
+            uint16* shortValues = (uint16*)values;
+            for (unsigned long i = 0; i < nitems; i++) {
+               retValues[i] = shortValues[i];
+            }
+         } else if (format == 32) {
+            unsigned long* longValues = (unsigned long*)values;
+            for (unsigned long i = 0; i < nitems; i++) {
+               retValues[i] = longValues[i];
+            }
+         } else {
+            NOT_IMPLEMENTED();
+         }
+      }
+   }
+
+   if (values != NULL) {
+      XFree(values);
+   }
+   return result;
+}
+
+
+} // namespace xutils
diff --git a/open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.hh b/open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.hh
new file mode 100644 (file)
index 0000000..f45520a
--- /dev/null
@@ -0,0 +1,39 @@
+/*********************************************************
+ * Copyright (c) 2025 Broadcom. All Rights Reserved.
+ * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation version 2.1 and no later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the Lesser GNU General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *********************************************************/
+
+#ifndef XUTILS_XUTILS_HH
+#define XUTILS_XUTILS_HH
+
+#include <gdkmm.h>
+#include <gtkmm.h>
+
+#include "stringxx/string.hh"
+
+
+namespace xutils {
+/* General property helpers */
+bool GetCardinal(Glib::RefPtr<const Gdk::Surface> surface,
+                 const utf::string& atomName,
+                 unsigned long& retValue);
+bool GetCardinalList(Glib::RefPtr<const Gdk::Surface> surface,
+                     const utf::string& atomName,
+                     std::vector<unsigned long>& retValues);
+} // namespace xutils
+
+#endif // XUTILS_XUTILS_HH