From: Kruti Pendharkar Date: Mon, 1 Dec 2025 07:06:48 +0000 (-0800) Subject: Add GTK4 support in open-vm-tools X-Git-Tag: stable-13.1.0~39 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=9ea6cadba89c57cc119164702aac2e167a1d4071;p=thirdparty%2Fopen-vm-tools.git Add GTK4 support in open-vm-tools --- diff --git a/open-vm-tools/configure.ac b/open-vm-tools/configure.ac index 38b8f6c0e..598df55e6 100644 --- a/open-vm-tools/configure.ac +++ b/open-vm-tools/configure.ac @@ -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") diff --git a/open-vm-tools/services/plugins/desktopEvents/x11Lock.c b/open-vm-tools/services/plugins/desktopEvents/x11Lock.c index 53b44bde1..3e7e5c8c5 100644 --- a/open-vm-tools/services/plugins/desktopEvents/x11Lock.c +++ b/open-vm-tools/services/plugins/desktopEvents/x11Lock.c @@ -29,11 +29,20 @@ #include #include -#include #include #include +#if GTK_MAJOR_VERSION == 4 +#include +#else +#include +#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"); diff --git a/open-vm-tools/services/plugins/dndcp/Makefile.am b/open-vm-tools/services/plugins/dndcp/Makefile.am index 5a864e7c0..e09387459 100644 --- a/open-vm-tools/services/plugins/dndcp/Makefile.am +++ b/open-vm-tools/services/plugins/dndcp/Makefile.am @@ -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 diff --git a/open-vm-tools/services/plugins/dndcp/copyPasteCompatX11.c b/open-vm-tools/services/plugins/dndcp/copyPasteCompatX11.c index 40e295a8d..337621efa 100644 --- a/open-vm-tools/services/plugins/dndcp/copyPasteCompatX11.c +++ b/open-vm-tools/services/plugins/dndcp/copyPasteCompatX11.c @@ -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 index 000000000..643d2c586 --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/copyPasteCompatX11GTK4.c @@ -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 +#include +#include + +#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; +} diff --git a/open-vm-tools/services/plugins/dndcp/copyPasteDnDX11.cpp b/open-vm-tools/services/plugins/dndcp/copyPasteDnDX11.cpp index 4756e0d38..4082108b1 100644 --- a/open-vm-tools/services/plugins/dndcp/copyPasteDnDX11.cpp +++ b/open-vm-tools/services/plugins/dndcp/copyPasteDnDX11.cpp @@ -28,6 +28,10 @@ #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 } } diff --git a/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.cpp b/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.cpp index 9f3491369..a4580eeca 100644 --- a/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.cpp +++ b/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.cpp @@ -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" diff --git a/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.h b/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.h index 436b057d6..44fab3213 100644 --- a/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.h +++ b/open-vm-tools/services/plugins/dndcp/copyPasteUIX11.h @@ -65,6 +65,10 @@ extern "C" { #define VMTOOLS_USE_LEGACY_GTK +#ifdef GTK4 +#undef VMTOOLS_USE_LEGACY_GTK +#include +#endif #ifdef VMTOOLS_USE_LEGACY_GTK #include #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& imgData); + void CopyPasteSendRtfValue(const Glib::RefPtr& result); + void CopyPasteSendFile(const Glib::RefPtr& result); + void CopyPasteRequestStreamCb(const Glib::RefPtr& result, + const Glib::RefPtr& clipboard); + void CopyPasteRequestTextCb(const Glib::RefPtr& result, + const Glib::RefPtr& clipboard); + void CopyPasteRequestImageCb(const Glib::RefPtr& result, + const Glib::RefPtr& 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 mDftClipboardPtr; + Glib::RefPtr mPrimClipboardPtr; + Glib::RefPtr mCpStream; +#endif #ifdef VMTOOLS_USE_LEGACY_GTK std::vector 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 index 000000000..1ba8db483 --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/copyPasteUIX11GTK4.cpp @@ -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 +#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 +#include + +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 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 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 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 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 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& 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& result, // IN + const Glib::RefPtr& 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& result, // IN + const Glib::RefPtr& clipboard) // IN +{ + try { + Glib::RefPtr image = clipboard->read_texture_finish(result); + if (image) { + Glib::RefPtr 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& result) // IN +{ + Glib::RefPtr 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& 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 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(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& result, // IN + const Glib::RefPtr& 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> provider; + Glib::RefPtr providerUnion; + Glib::RefPtr fileListGnome; + Glib::RefPtr fileListKde; + Glib::RefPtr 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(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> 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 providerUnion; + Glib::RefPtr textData; + Glib::RefPtr 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 imgData = Glib::Bytes::create(buf, sz); + + try { + Glib::RefPtr 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(arg); + pthread_mutex_lock(¶ms->fileBlockMutex); + while (true) { + g_debug("%s: waiting signal\n", __FUNCTION__); + pthread_cond_wait(¶ms->fileBlockCond, ¶ms->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(¶ms->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; +} + diff --git a/open-vm-tools/services/plugins/dndcp/dnd/dnd.h b/open-vm-tools/services/plugins/dndcp/dnd/dnd.h index f15c6b228..82ec3fa61 100644 --- a/open-vm-tools/services/plugins/dndcp/dnd/dnd.h +++ b/open-vm-tools/services/plugins/dndcp/dnd/dnd.h @@ -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 diff --git a/open-vm-tools/services/plugins/dndcp/dndGuestBase/copyPasteDnDX11.h b/open-vm-tools/services/plugins/dndcp/dndGuestBase/copyPasteDnDX11.h index 9acc3cc6a..ed3b2c5e5 100644 --- a/open-vm-tools/services/plugins/dndcp/dndGuestBase/copyPasteDnDX11.h +++ b/open-vm-tools/services/plugins/dndcp/dndGuestBase/copyPasteDnDX11.h @@ -29,8 +29,16 @@ #define VMTOOLS_USE_LEGACY_GTK +#ifdef GTK4 +#undef VMTOOLS_USE_LEGACY_GTK +#endif #include "dnd.h" /* for DnDBlockControl */ +#ifdef GTK4 +#include +#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 index 000000000..f18c7e2f1 --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/dndGuestBase/dndUIX11GTK4.h @@ -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 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 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 index 000000000..e576d3eb4 --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/dndGuestBase/dragDetWndX11GTK4.h @@ -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 +#include +#include +#include + +#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 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 diff --git a/open-vm-tools/services/plugins/dndcp/dndGuestBase/guestDnD.hh b/open-vm-tools/services/plugins/dndcp/dndGuestBase/guestDnD.hh index 42f5b4f88..f04b4925e 100644 --- a/open-vm-tools/services/plugins/dndcp/dndGuestBase/guestDnD.hh +++ b/open-vm-tools/services/plugins/dndcp/dndGuestBase/guestDnD.hh @@ -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 diff --git a/open-vm-tools/services/plugins/dndcp/dndPluginIntX11.h b/open-vm-tools/services/plugins/dndcp/dndPluginIntX11.h index 0f2c31cce..f60f723cf 100644 --- a/open-vm-tools/services/plugins/dndcp/dndPluginIntX11.h +++ b/open-vm-tools/services/plugins/dndcp/dndPluginIntX11.h @@ -27,9 +27,15 @@ #define VMTOOLS_USE_LEGACY_GTK +#ifdef GTK4 +#undef VMTOOLS_USE_LEGACY_GTK +#endif #include #include +#ifdef GTK4 +#include +#endif #ifdef VMTOOLS_USE_LEGACY_GTK #include @@ -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 index 000000000..2a0e160fe --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/dndUIX11GTK4.cpp @@ -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 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> providerVec; + Glib::RefPtr fileListGnome; + Glib::RefPtr 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 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 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 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 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 index 000000000..9bda763bf --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/dragDetWndX11GTK4.cpp @@ -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 + +extern "C" { +#include /* for XTest*() */ +#include +#include +#include +} + + +/** + * + * Constructor. + */ + +DragDetWnd::DragDetWnd(int UseUInput_fd) + : m_isVisible(false), + mWnd(NULL), + mSource(NULL), + mTarget(NULL), + mUseUInput(false), + mScreenWidth(0), + mScreenHeight(0) +{ + std::vector 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(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 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(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(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 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 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 display = Gdk::Display::get_default(); + int width = 0; + int height = 0; + g_debug("%s: enter\n", __FUNCTION__); + if (display) { + Glib::RefPtr 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 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 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 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 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 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(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 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 index 000000000..853078d94 --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.cc @@ -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 +#include +#include +#include + +#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 surface, // IN: Surface + const utf::string& atomName, // IN: Atom name + unsigned long& retValue) // OUT: Return value +{ + ASSERT(surface); + ASSERT(!atomName.empty()); + + std::vector 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 surface, // IN: Surface + const utf::string& atomName, // IN: Atom name + std::vector& retValues) // OUT: Return values +{ + ASSERT(surface); + ASSERT(surface->get_display()); + ASSERT(!atomName.empty()); + + GdkDisplay* gdkDisplay = const_cast(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 index 000000000..f45520ac9 --- /dev/null +++ b/open-vm-tools/services/plugins/dndcp/xutils/xutilsGTK4.hh @@ -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 +#include + +#include "stringxx/string.hh" + + +namespace xutils { +/* General property helpers */ +bool GetCardinal(Glib::RefPtr surface, + const utf::string& atomName, + unsigned long& retValue); +bool GetCardinalList(Glib::RefPtr surface, + const utf::string& atomName, + std::vector& retValues); +} // namespace xutils + +#endif // XUTILS_XUTILS_HH