/* Copyright (C) 2010-2011 Red Hat, Inc. This library 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; either version 2.1 of the License, or (at your option) any later version. This library 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 GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see . */ #include "config.h" #include #include #ifdef HAVE_X11_XKBLIB_H #include #endif #ifdef GDK_WINDOWING_X11 #include #include #endif #ifdef G_OS_WIN32 #include #include #ifndef MAPVK_VK_TO_VSC /* may be undefined in older mingw-headers */ #define MAPVK_VK_TO_VSC 0 #endif #endif #ifdef HAVE_PHODAV_VIRTUAL #include #endif #include #include #include "desktop-integration.h" #include "spice-common.h" #include "spice-gtk-session.h" #include "spice-gtk-session-priv.h" #include "spice-session-priv.h" #include "spice-util-priv.h" #include "spice-channel-priv.h" #define CLIPBOARD_LAST (VD_AGENT_CLIPBOARD_SELECTION_SECONDARY + 1) struct _SpiceGtkSessionPrivate { SpiceSession *session; /* Clipboard related */ gboolean auto_clipboard_enable; SpiceMainChannel *main; GtkClipboard *clipboard; GtkClipboard *clipboard_primary; GtkTargetEntry *clip_targets[CLIPBOARD_LAST]; guint nclip_targets[CLIPBOARD_LAST]; GdkAtom *atoms[CLIPBOARD_LAST]; guint n_atoms[CLIPBOARD_LAST]; gboolean clip_hasdata[CLIPBOARD_LAST]; gboolean clip_grabbed[CLIPBOARD_LAST]; gboolean clipboard_by_guest[CLIPBOARD_LAST]; guint clipboard_release_delay[CLIPBOARD_LAST]; /* TODO: maybe add a way of restoring this? */ GHashTable *cb_shared_files; /* auto-usbredir related */ gboolean auto_usbredir_enable; int auto_usbredir_reqs; gboolean pointer_grabbed; gboolean keyboard_has_focus; gboolean mouse_has_pointer; gboolean sync_modifiers; }; /** * SECTION:spice-gtk-session * @short_description: handles GTK connection details * @title: Spice GTK Session * @section_id: * @see_also: #SpiceSession, and the GTK widget #SpiceDisplay * @stability: Stable * @include: spice-client-gtk.h * * The #SpiceGtkSession class is the spice-client-gtk counter part of * #SpiceSession. It contains functionality which should be handled per * session rather then per #SpiceDisplay (one session can have multiple * displays), but which cannot live in #SpiceSession as it depends on * GTK. For example the clipboard functionality. * * There should always be a 1:1 relation between #SpiceGtkSession objects * and #SpiceSession objects. Therefor there is no spice_gtk_session_new, * instead there is spice_gtk_session_get() which ensures this 1:1 relation. * * Client and guest clipboards will be shared automatically if * #SpiceGtkSession:auto-clipboard is set to #TRUE. Alternatively, you * can send / receive clipboard data from client to guest with * spice_gtk_session_copy_to_guest() / spice_gtk_session_paste_from_guest(). */ /* ------------------------------------------------------------------ */ /* Prototypes for private functions */ static void clipboard_release(SpiceGtkSession *self, guint selection); static void clipboard_owner_change(GtkClipboard *clipboard, GdkEventOwnerChange *event, gpointer user_data); static void channel_new(SpiceSession *session, SpiceChannel *channel, gpointer user_data); static void channel_destroy(SpiceSession *session, SpiceChannel *channel, gpointer user_data); static gboolean read_only(SpiceGtkSession *self); G_DEFINE_TYPE_WITH_PRIVATE(SpiceGtkSession, spice_gtk_session, G_TYPE_OBJECT) /* Properties */ enum { PROP_0, PROP_SESSION, PROP_AUTO_CLIPBOARD, PROP_AUTO_USBREDIR, PROP_POINTER_GRABBED, PROP_SYNC_MODIFIERS, }; static guint32 get_keyboard_lock_modifiers(void) { guint32 modifiers = 0; /* Ignore GLib's too-new warnings */ GdkKeymap *keyboard = gdk_keymap_get_for_display(gdk_display_get_default()); if (gdk_keymap_get_caps_lock_state(keyboard)) { modifiers |= SPICE_INPUTS_CAPS_LOCK; } if (gdk_keymap_get_num_lock_state(keyboard)) { modifiers |= SPICE_INPUTS_NUM_LOCK; } if (gdk_keymap_get_scroll_lock_state(keyboard)) { modifiers |= SPICE_INPUTS_SCROLL_LOCK; } return modifiers; } static void spice_gtk_session_sync_keyboard_modifiers_for_channel(SpiceGtkSession *self, SpiceInputsChannel* inputs, gboolean force) { guint32 guest_modifiers = 0, client_modifiers = 0; g_return_if_fail(SPICE_IS_INPUTS_CHANNEL(inputs)); if (SPICE_IS_GTK_SESSION(self) && !self->priv->sync_modifiers) { SPICE_DEBUG("Syncing modifiers is disabled"); return; } g_object_get(inputs, "key-modifiers", &guest_modifiers, NULL); client_modifiers = get_keyboard_lock_modifiers(); if (force || client_modifiers != guest_modifiers) { CHANNEL_DEBUG(inputs, "client_modifiers:0x%x, guest_modifiers:0x%x", client_modifiers, guest_modifiers); spice_inputs_channel_set_key_locks(inputs, client_modifiers); } } static void keymap_modifiers_changed(GdkKeymap *keymap, gpointer data) { SpiceGtkSession *self = data; /* set_key_locks() is inherently racy, but no need to sync modifiers * if we have focus as the regular keypress/keyrelease will have set * the expected modifiers state in the guest. */ if (self->priv->keyboard_has_focus) { return; } spice_gtk_session_sync_keyboard_modifiers(self); } static void guest_modifiers_changed(SpiceInputsChannel *inputs, gpointer data) { SpiceGtkSession *self = data; spice_gtk_session_sync_keyboard_modifiers_for_channel(self, inputs, FALSE); } static void spice_gtk_session_init(SpiceGtkSession *self) { SpiceGtkSessionPrivate *s; GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default()); s = self->priv = spice_gtk_session_get_instance_private(self); s->cb_shared_files = g_hash_table_new_full(g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, /* unref GFile */ g_free /* free gchar * */ ); s->clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); g_signal_connect(G_OBJECT(s->clipboard), "owner-change", G_CALLBACK(clipboard_owner_change), self); s->clipboard_primary = gtk_clipboard_get(GDK_SELECTION_PRIMARY); g_signal_connect(G_OBJECT(s->clipboard_primary), "owner-change", G_CALLBACK(clipboard_owner_change), self); spice_g_signal_connect_object(keymap, "state-changed", G_CALLBACK(keymap_modifiers_changed), self, 0); } static void spice_gtk_session_constructed(GObject *gobject) { SpiceGtkSession *self; SpiceGtkSessionPrivate *s; GList *list; GList *it; self = SPICE_GTK_SESSION(gobject); s = self->priv; if (!s->session) g_error("SpiceGtKSession constructed without a session"); g_signal_connect(s->session, "channel-new", G_CALLBACK(channel_new), self); g_signal_connect(s->session, "channel-destroy", G_CALLBACK(channel_destroy), self); list = spice_session_get_channels(s->session); for (it = g_list_first(list); it != NULL; it = g_list_next(it)) { channel_new(s->session, it->data, (gpointer*)self); } g_list_free(list); } static void spice_gtk_session_dispose(GObject *gobject) { SpiceGtkSession *self = SPICE_GTK_SESSION(gobject); SpiceGtkSessionPrivate *s = self->priv; /* release stuff */ if (s->clipboard) { g_signal_handlers_disconnect_by_func(s->clipboard, G_CALLBACK(clipboard_owner_change), self); s->clipboard = NULL; } if (s->clipboard_primary) { g_signal_handlers_disconnect_by_func(s->clipboard_primary, G_CALLBACK(clipboard_owner_change), self); s->clipboard_primary = NULL; } if (s->session) { g_signal_handlers_disconnect_by_func(s->session, G_CALLBACK(channel_new), self); g_signal_handlers_disconnect_by_func(s->session, G_CALLBACK(channel_destroy), self); s->session = NULL; } g_clear_pointer(&s->cb_shared_files, g_hash_table_destroy); /* Chain up to the parent class */ if (G_OBJECT_CLASS(spice_gtk_session_parent_class)->dispose) G_OBJECT_CLASS(spice_gtk_session_parent_class)->dispose(gobject); } static void clipboard_release_delay_remove(SpiceGtkSession *self, guint selection, gboolean release_if_delayed) { SpiceGtkSessionPrivate *s = self->priv; if (!s->clipboard_release_delay[selection]) { return; } if (release_if_delayed) { SPICE_DEBUG("delayed clipboard release, sel:%u", selection); clipboard_release(self, selection); } g_source_remove(s->clipboard_release_delay[selection]); s->clipboard_release_delay[selection] = 0; } static void spice_gtk_session_finalize(GObject *gobject) { SpiceGtkSession *self = SPICE_GTK_SESSION(gobject); SpiceGtkSessionPrivate *s = self->priv; int i; /* release stuff */ for (i = 0; i < CLIPBOARD_LAST; ++i) { g_clear_pointer(&s->clip_targets[i], g_free); clipboard_release_delay_remove(self, i, true); g_clear_pointer(&s->atoms[i], g_free); s->n_atoms[i] = 0; } /* Chain up to the parent class */ if (G_OBJECT_CLASS(spice_gtk_session_parent_class)->finalize) G_OBJECT_CLASS(spice_gtk_session_parent_class)->finalize(gobject); } static void spice_gtk_session_get_property(GObject *gobject, guint prop_id, GValue *value, GParamSpec *pspec) { SpiceGtkSession *self = SPICE_GTK_SESSION(gobject); SpiceGtkSessionPrivate *s = self->priv; switch (prop_id) { case PROP_SESSION: g_value_set_object(value, s->session); break; case PROP_AUTO_CLIPBOARD: g_value_set_boolean(value, s->auto_clipboard_enable); break; case PROP_AUTO_USBREDIR: g_value_set_boolean(value, s->auto_usbredir_enable); break; case PROP_POINTER_GRABBED: g_value_set_boolean(value, s->pointer_grabbed); break; case PROP_SYNC_MODIFIERS: g_value_set_boolean(value, s->sync_modifiers); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec); break; } } static void spice_gtk_session_set_property(GObject *gobject, guint prop_id, const GValue *value, GParamSpec *pspec) { SpiceGtkSession *self = SPICE_GTK_SESSION(gobject); SpiceGtkSessionPrivate *s = self->priv; switch (prop_id) { case PROP_SESSION: s->session = g_value_get_object(value); break; case PROP_AUTO_CLIPBOARD: s->auto_clipboard_enable = g_value_get_boolean(value); break; case PROP_AUTO_USBREDIR: { SpiceDesktopIntegration *desktop_int; gboolean orig_value = s->auto_usbredir_enable; s->auto_usbredir_enable = g_value_get_boolean(value); if (s->auto_usbredir_enable == orig_value) break; if (s->auto_usbredir_reqs) { SpiceUsbDeviceManager *manager = spice_usb_device_manager_get(s->session, NULL); if (!manager) break; g_object_set(manager, "auto-connect", s->auto_usbredir_enable, NULL); desktop_int = spice_desktop_integration_get(s->session); if (s->auto_usbredir_enable) spice_desktop_integration_inhibit_automount(desktop_int); else spice_desktop_integration_uninhibit_automount(desktop_int); } break; } case PROP_SYNC_MODIFIERS: s->sync_modifiers = g_value_get_boolean(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec); break; } } static void spice_gtk_session_class_init(SpiceGtkSessionClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); gobject_class->constructed = spice_gtk_session_constructed; gobject_class->dispose = spice_gtk_session_dispose; gobject_class->finalize = spice_gtk_session_finalize; gobject_class->get_property = spice_gtk_session_get_property; gobject_class->set_property = spice_gtk_session_set_property; /** * SpiceGtkSession:session: * * #SpiceSession this #SpiceGtkSession is associated with * * Since: 0.8 **/ g_object_class_install_property (gobject_class, PROP_SESSION, g_param_spec_object("session", "Session", "SpiceSession", SPICE_TYPE_SESSION, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * SpiceGtkSession:auto-clipboard: * * When this is true the clipboard gets automatically shared between host * and guest. * * Since: 0.8 **/ g_object_class_install_property (gobject_class, PROP_AUTO_CLIPBOARD, g_param_spec_boolean("auto-clipboard", "Auto clipboard", "Automatically relay clipboard changes between " "host and guest.", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /** * SpiceGtkSession:auto-usbredir: * * Automatically redirect newly plugged in USB devices. Note the auto * redirection only happens when a #SpiceDisplay associated with the * session had keyboard focus. * * Since: 0.8 **/ g_object_class_install_property (gobject_class, PROP_AUTO_USBREDIR, g_param_spec_boolean("auto-usbredir", "Auto USB Redirection", "Automatically redirect newly plugged in USB" "Devices to the guest.", FALSE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); /** * SpiceGtkSession:pointer-grabbed: * * Returns %TRUE if the pointer is currently grabbed by this session. * * Since: 0.27 **/ g_object_class_install_property (gobject_class, PROP_POINTER_GRABBED, g_param_spec_boolean("pointer-grabbed", "Pointer grabbed", "Whether the pointer is grabbed", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); /** * SpiceGtkSession:sync-modifiers: * * Automatically sync modifiers (Caps, Num and Scroll locks) with the guest. * * Since: 0.32 **/ g_object_class_install_property (gobject_class, PROP_SYNC_MODIFIERS, g_param_spec_boolean("sync-modifiers", "Sync modifiers", "Automatically sync modifiers", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); } /* ---------------------------------------------------------------- */ /* private functions (clipboard related) */ static GtkClipboard* get_clipboard_from_selection(SpiceGtkSessionPrivate *s, guint selection) { if (selection == VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) { return s->clipboard; } else if (selection == VD_AGENT_CLIPBOARD_SELECTION_PRIMARY) { return s->clipboard_primary; } else { g_warning("Unhandled clipboard selection: %u", selection); return NULL; } } static gint get_selection_from_clipboard(SpiceGtkSessionPrivate *s, GtkClipboard* cb) { if (cb == s->clipboard) { return VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; } else if (cb == s->clipboard_primary) { return VD_AGENT_CLIPBOARD_SELECTION_PRIMARY; } else { g_warning("Unhandled clipboard"); return -1; } } static const struct { const char *xatom; uint32_t vdagent; } atom2agent[] = { { .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT, .xatom = "UTF8_STRING", },{ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT, .xatom = "text/plain;charset=utf-8" },{ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT, .xatom = "STRING" },{ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT, .xatom = "TEXT" },{ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT, .xatom = "text/plain" },{ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_PNG, .xatom = "image/png" },{ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP, .xatom = "image/bmp" },{ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP, .xatom = "image/x-bmp" },{ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP, .xatom = "image/x-MS-bmp" },{ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP, .xatom = "image/x-win-bitmap" },{ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_TIFF, .xatom = "image/tiff" },{ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_JPG, .xatom = "image/jpeg" },{ .vdagent = VD_AGENT_CLIPBOARD_FILE_LIST, .xatom = "text/uri-list" } }; static GWeakRef* get_weak_ref(gpointer object) { GWeakRef *weakref = g_new(GWeakRef, 1); g_weak_ref_init(weakref, object); return weakref; } static gpointer free_weak_ref(gpointer data) { GWeakRef *weakref = data; gpointer object = g_weak_ref_get(weakref); g_weak_ref_clear(weakref); g_free(weakref); if (object != NULL) { /* The main reference still exists as object is not NULL, so we can * remove the strong reference given by g_weak_ref_get */ g_object_unref(object); } return object; } #ifdef HAVE_PHODAV_VIRTUAL static SpiceWebdavChannel *clipboard_get_open_webdav(SpiceSession *session) { GList *list, *l; SpiceChannel *channel = NULL; gboolean open = FALSE; g_return_val_if_fail(session != NULL, NULL); list = spice_session_get_channels(session); for (l = g_list_first(list); l != NULL; l = g_list_next(l)) { channel = l->data; if (!SPICE_IS_WEBDAV_CHANNEL(channel)) { continue; } g_object_get(channel, "port-opened", &open, NULL); break; } g_list_free(list); return open ? SPICE_WEBDAV_CHANNEL(channel) : NULL; } static GdkAtom clipboard_find_atom(SpiceGtkSessionPrivate *s, guint selection, GdkAtom a) { for (int i = 0; i < s->n_atoms[selection]; i++) { if (s->atoms[selection][i] == a) { return a; } } return GDK_NONE; } #endif static void clipboard_get_targets(GtkClipboard *clipboard, GdkAtom *atoms, gint n_atoms, gpointer user_data) { SpiceGtkSession *self = free_weak_ref(user_data); SPICE_DEBUG("%s:", __FUNCTION__); if (self == NULL) return; g_return_if_fail(SPICE_IS_GTK_SESSION(self)); if (atoms == NULL) { SPICE_DEBUG("Retrieving the clipboard data has failed"); return; } SpiceGtkSessionPrivate *s = self->priv; guint32 types[SPICE_N_ELEMENTS(atom2agent)] = { 0 }; gint num_types; int a; int selection; if (s->main == NULL) return; selection = get_selection_from_clipboard(s, clipboard); g_return_if_fail(selection != -1); /* GTK+ does seem to cache atoms, but not for Wayland */ g_free(s->atoms[selection]); s->atoms[selection] = g_memdup(atoms, n_atoms * sizeof(GdkAtom)); s->n_atoms[selection] = n_atoms; if (s->clip_grabbed[selection]) { SPICE_DEBUG("Clipboard is already grabbed, re-grab: %d atoms", n_atoms); } /* Set all Atoms that matches our current protocol implementation */ num_types = 0; for (a = 0; a < n_atoms; a++) { guint m; gchar *name = gdk_atom_name(atoms[a]); SPICE_DEBUG(" \"%s\"", name); for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) { guint t; if (strcasecmp(name, atom2agent[m].xatom) != 0) { continue; } if (atom2agent[m].vdagent == VD_AGENT_CLIPBOARD_FILE_LIST) { #ifdef HAVE_PHODAV_VIRTUAL if (!clipboard_get_open_webdav(s->session)) { SPICE_DEBUG("Received %s target, but the clipboard webdav channel " "isn't available, skipping", atom2agent[m].xatom); break; } #else break; #endif } /* check if type is already in list */ for (t = 0; t < num_types; t++) { if (types[t] == atom2agent[m].vdagent) { break; } } if (t == num_types) { /* add type to empty slot */ types[t] = atom2agent[m].vdagent; num_types++; } } g_free(name); } if (num_types == 0) { SPICE_DEBUG("No GdkAtoms will be sent from %d", n_atoms); return; } s->clip_grabbed[selection] = TRUE; if (spice_main_channel_agent_test_capability(s->main, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND)) spice_main_channel_clipboard_selection_grab(s->main, selection, types, num_types); /* Sending a grab causes the agent to do an implicit release */ s->nclip_targets[selection] = 0; } /* Callback for every owner-change event for given @clipboard. * This event is triggered in different ways depending on the environment of * the Client, some examples: * * Situation 1: When another application on the client machine is holding and * changing the clipboard. If client is on Wayland, spice-gtk only receives the * related GtkClipboard::owner-changed event after focus-in event on Spice * widget; On X11, we will receive it at the moment the clipboard data has been * changed in by other application. * * Situation 2: When spice-gtk holds the focus and is changing the clipboard by * either setting new content information with gtk_clipboard_set_with_owner() or * clearing up old content with gtk_clipboard_clear(). The main difference between * Wayland and X11 is that on X11, gtk_clipboard_clear() sets the owner to none, which * emits owner-change event; On Wayland that does not happen as spice-gtk still is * the owner of the clipboard. */ static void clipboard_owner_change(GtkClipboard *clipboard, GdkEventOwnerChange *event, gpointer user_data) { g_return_if_fail(SPICE_IS_GTK_SESSION(user_data)); SpiceGtkSession *self = user_data; SpiceGtkSessionPrivate *s = self->priv; int selection; selection = get_selection_from_clipboard(s, clipboard); g_return_if_fail(selection != -1); if (s->main == NULL) { return; } g_clear_pointer(&s->atoms[selection], g_free); s->n_atoms[selection] = 0; if (event->reason != GDK_OWNER_CHANGE_NEW_OWNER) { if (s->clip_grabbed[selection]) { /* grab was sent to the agent, so release it */ s->clip_grabbed[selection] = FALSE; if (spice_main_channel_agent_test_capability(s->main, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND)) { spice_main_channel_clipboard_selection_release(s->main, selection); } } s->clip_hasdata[selection] = FALSE; return; } /* This situation happens when clipboard is being set by us (grab message) */ if (gtk_clipboard_get_owner(clipboard) == G_OBJECT(self)) { return; } s->clipboard_by_guest[selection] = FALSE; #ifdef GDK_WINDOWING_X11 if (!event->owner && GDK_IS_X11_DISPLAY(gdk_display_get_default())) { s->clip_hasdata[selection] = FALSE; return; } #endif s->clip_hasdata[selection] = TRUE; if (s->auto_clipboard_enable && !read_only(self)) gtk_clipboard_request_targets(clipboard, clipboard_get_targets, get_weak_ref(self)); } typedef struct { SpiceGtkSession *self; GMainLoop *loop; GtkSelectionData *selection_data; guint info; guint selection; } RunInfo; static void clipboard_got_from_guest(SpiceMainChannel *main, guint selection, guint type, const guchar *data, guint size, gpointer user_data) { RunInfo *ri = user_data; SpiceGtkSessionPrivate *s = ri->self->priv; gchar *conv = NULL; g_return_if_fail(selection == ri->selection); SPICE_DEBUG("clipboard got data"); if (atom2agent[ri->info].vdagent == VD_AGENT_CLIPBOARD_UTF8_TEXT) { /* on windows, gtk+ would already convert to LF endings, but not on unix */ if (spice_main_channel_agent_test_capability(s->main, VD_AGENT_CAP_GUEST_LINEEND_CRLF)) { conv = spice_dos2unix((gchar*)data, size); size = strlen(conv); } gtk_selection_data_set_text(ri->selection_data, conv ?: (gchar*)data, size); } else { gtk_selection_data_set(ri->selection_data, gdk_atom_intern_static_string(atom2agent[ri->info].xatom), 8, data, size); } if (g_main_loop_is_running (ri->loop)) g_main_loop_quit (ri->loop); g_free(conv); } static void clipboard_agent_connected(RunInfo *ri) { g_warning("agent status changed, cancel clipboard request"); if (g_main_loop_is_running(ri->loop)) g_main_loop_quit(ri->loop); } static void clipboard_get(GtkClipboard *clipboard, GtkSelectionData *selection_data, guint info, gpointer user_data) { g_return_if_fail(SPICE_IS_GTK_SESSION(user_data)); RunInfo ri = { NULL, }; SpiceGtkSession *self = user_data; SpiceGtkSessionPrivate *s = self->priv; gboolean agent_connected = FALSE; gulong clipboard_handler; gulong agent_handler; int selection; SPICE_DEBUG("clipboard get"); selection = get_selection_from_clipboard(s, clipboard); g_return_if_fail(selection != -1); g_return_if_fail(info < SPICE_N_ELEMENTS(atom2agent)); g_return_if_fail(s->main != NULL); if (s->clipboard_release_delay[selection]) { SPICE_DEBUG("not requesting data from guest during delayed release"); return; } ri.selection_data = selection_data; ri.info = info; ri.loop = g_main_loop_new(NULL, FALSE); ri.selection = selection; ri.self = self; clipboard_handler = g_signal_connect(s->main, "main-clipboard-selection", G_CALLBACK(clipboard_got_from_guest), &ri); agent_handler = g_signal_connect_swapped(s->main, "notify::agent-connected", G_CALLBACK(clipboard_agent_connected), &ri); spice_main_channel_clipboard_selection_request(s->main, selection, atom2agent[info].vdagent); g_object_get(s->main, "agent-connected", &agent_connected, NULL); if (!agent_connected) { SPICE_DEBUG("canceled clipboard_get, before running loop"); goto cleanup; } /* This is modeled on the implementation of gtk_dialog_run() even though * these thread functions are deprecated and appears to be needed to avoid * dead-lock from gtk_dialog_run(). */ G_GNUC_BEGIN_IGNORE_DEPRECATIONS gdk_threads_leave(); g_main_loop_run(ri.loop); gdk_threads_enter(); G_GNUC_END_IGNORE_DEPRECATIONS cleanup: g_clear_pointer(&ri.loop, g_main_loop_unref); g_signal_handler_disconnect(s->main, clipboard_handler); g_signal_handler_disconnect(s->main, agent_handler); } static void clipboard_clear(GtkClipboard *clipboard, gpointer user_data) { SPICE_DEBUG("clipboard_clear"); /* We watch for clipboard ownership changes and act on those, so we don't need to do anything here */ } static gboolean clipboard_grab(SpiceMainChannel *main, guint selection, guint32* types, guint32 ntypes, gpointer user_data) { g_return_val_if_fail(SPICE_IS_GTK_SESSION(user_data), FALSE); SpiceGtkSession *self = user_data; SpiceGtkSessionPrivate *s = self->priv; GtkTargetEntry targets[SPICE_N_ELEMENTS(atom2agent)]; gboolean target_selected[SPICE_N_ELEMENTS(atom2agent)] = { FALSE, }; gboolean found; GtkClipboard* cb; int m, n; int num_targets = 0; clipboard_release_delay_remove(self, selection, false); cb = get_clipboard_from_selection(s, selection); g_return_val_if_fail(cb != NULL, FALSE); for (n = 0; n < ntypes; ++n) { found = FALSE; for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) { if (atom2agent[m].vdagent == types[n] && !target_selected[m]) { found = TRUE; g_return_val_if_fail(num_targets < SPICE_N_ELEMENTS(atom2agent), FALSE); targets[num_targets].target = (gchar*)atom2agent[m].xatom; targets[num_targets].info = m; target_selected[m] = TRUE; num_targets++; } } if (!found) { g_warning("clipboard: couldn't find a matching type for: %u", types[n]); } } g_free(s->clip_targets[selection]); s->nclip_targets[selection] = num_targets; s->clip_targets[selection] = g_memdup(targets, sizeof(GtkTargetEntry) * num_targets); /* Receiving a grab implies we've released our own grab */ s->clip_grabbed[selection] = FALSE; if (read_only(self) || !s->auto_clipboard_enable || s->nclip_targets[selection] == 0) { return TRUE; } if (!gtk_clipboard_set_with_owner(cb, targets, num_targets, clipboard_get, clipboard_clear, G_OBJECT(self))) { g_warning("clipboard grab failed"); return FALSE; } s->clipboard_by_guest[selection] = TRUE; s->clip_hasdata[selection] = FALSE; return TRUE; } static gboolean check_clipboard_size_limits(SpiceGtkSession *session, gint clipboard_len) { int max_clipboard; g_object_get(session->priv->main, "max-clipboard", &max_clipboard, NULL); if (max_clipboard != -1 && clipboard_len > max_clipboard) { g_warning("discarded clipboard of size %d (max: %d)", clipboard_len, max_clipboard); return FALSE; } else if (clipboard_len <= 0) { SPICE_DEBUG("discarding empty clipboard"); return FALSE; } return TRUE; } /* This will convert line endings if needed (between Windows/Unix conventions), * and will make sure 'len' does not take into account any trailing \0 as this could * cause some confusion guest side. * The 'len' argument will be modified by this function to the length of the modified * string */ static char *fixup_clipboard_text(SpiceGtkSession *self, const char *text, int *len) { char *conv = NULL; if (spice_main_channel_agent_test_capability(self->priv->main, VD_AGENT_CAP_GUEST_LINEEND_CRLF)) { conv = spice_unix2dos(text, *len); *len = strlen(conv); } else { /* On Windows, with some versions of gtk+, GtkSelectionData::length * will include the final '\0'. When a string with this trailing '\0' * is pasted in some linux applications, it will be pasted as or * as an invisible character, which is unwanted. Ensure the length we * send to the agent does not include any trailing '\0' * This is gtk+ bug https://bugzilla.gnome.org/show_bug.cgi?id=734670 */ *len = strlen(text); } return conv; } static void clipboard_received_text_cb(GtkClipboard *clipboard, const gchar *text, gpointer user_data) { SpiceGtkSession *self = free_weak_ref(user_data); char *conv = NULL; int len = 0; int selection; const guchar *data = NULL; if (self == NULL) return; selection = get_selection_from_clipboard(self->priv, clipboard); g_return_if_fail(selection != -1); if (text == NULL) { SPICE_DEBUG("Failed to retrieve clipboard text"); goto notify_agent; } g_return_if_fail(SPICE_IS_GTK_SESSION(self)); len = strlen(text); if (!check_clipboard_size_limits(self, len)) { SPICE_DEBUG("Failed size limits of clipboard text (%d bytes)", len); goto notify_agent; } /* gtk+ internal utf8 newline is always LF, even on windows */ conv = fixup_clipboard_text(self, text, &len); if (!check_clipboard_size_limits(self, len)) { SPICE_DEBUG("Failed size limits of clipboard text (%d bytes)", len); goto notify_agent; } data = (const guchar *) (conv != NULL ? conv : text); notify_agent: spice_main_channel_clipboard_selection_notify(self->priv->main, selection, VD_AGENT_CLIPBOARD_UTF8_TEXT, data, (data != NULL) ? len : 0); g_free(conv); } #ifdef HAVE_PHODAV_VIRTUAL /* returns path to @file under @root in clipboard phodav server, or NULL on error */ static gchar *clipboard_webdav_share_file(PhodavVirtualDir *root, GFile *file) { gchar *uuid; PhodavVirtualDir *dir; GError *err = NULL; /* separate directory is created for each file, * as we want to preserve the original filename and avoid conflicts */ for (guint i = 0; i < 8; i++) { uuid = g_uuid_string_random(); gchar *dir_path = g_strdup_printf(SPICE_WEBDAV_CLIPBOARD_FOLDER_PATH "/%s", uuid); dir = phodav_virtual_dir_new_dir(root, dir_path, &err); g_free(dir_path); if (!err) { break; } g_clear_pointer(&uuid, g_free); if (!g_error_matches(err, G_IO_ERROR, G_IO_ERROR_EXISTS)) { g_warning("failed to create phodav virtual dir: %s", err->message); g_error_free(err); return NULL; } g_clear_error(&err); } if (!dir) { g_warning("failed to create phodav virtual dir: all attempts failed"); return NULL; } phodav_virtual_dir_attach_real_child(dir, file); g_object_unref(dir); gchar *base = g_file_get_basename(file); gchar *path = g_strdup_printf(SPICE_WEBDAV_CLIPBOARD_FOLDER_PATH "/%s/%s", uuid, base); g_free(uuid); g_free(base); return path; } /* join all strings in @strv into a new char array, * including all terminating NULL-chars */ static gchar *strv_concat(gchar **strv, gsize *size_out) { gchar **str_p, *arr, *curr; g_return_val_if_fail(strv && size_out, NULL); for (str_p = strv, *size_out = 0; *str_p != NULL; str_p++) { *size_out += strlen(*str_p) + 1; } arr = g_malloc(*size_out); for (str_p = strv, curr = arr; *str_p != NULL; str_p++) { curr = g_stpcpy(curr, *str_p) + 1; } return arr; } /* if not done alreay, share all files in @uris using the webdav server * and return a new buffer with VD_AGENT_CLIPBOARD_FILE_LIST data */ static gchar *strv_uris_transform_to_data(SpiceGtkSessionPrivate *s, gchar **uris, gsize *size_out, GdkDragAction action) { SpiceWebdavChannel *webdav; /* if there's version mismatch between spice-client-gtk and spice-client-glib, * "webdav-server" property might not be present, so phodav must be initialized to NULL */ PhodavServer *phodav = NULL; PhodavVirtualDir *root; gchar **uri_ptr, *path, **paths, *data; GFile *file; guint n; *size_out = 0; if (!uris || g_strv_length(uris) < 1) { return NULL; } webdav = clipboard_get_open_webdav(s->session); if (!webdav) { SPICE_DEBUG("Received uris, but no webdav channel"); return NULL; } g_object_get(s->session, "webdav-server", &phodav, NULL); if (!phodav) { return NULL; } g_object_get(phodav, "root-file", &root, NULL); g_object_unref(phodav); paths = g_new0(gchar *, g_strv_length(uris) + 2); paths[0] = action == GDK_ACTION_MOVE ? "cut" : "copy"; n = 1; for (uri_ptr = uris; *uri_ptr != NULL; uri_ptr++) { file = g_file_new_for_uri(*uri_ptr); /* clipboard data is usually requested multiple times for no obvious reasons * (clipboar managers to blame?), we don't want to create multiple dirs for the same file */ path = g_hash_table_lookup(s->cb_shared_files, file); if (path) { SPICE_DEBUG("found %s with path %s", *uri_ptr, path); g_object_unref(file); } else { path = clipboard_webdav_share_file(root, file); g_return_val_if_fail(path != NULL, NULL); SPICE_DEBUG("publishing %s under %s", *uri_ptr, path); /* file and path gets freed once the hash table gets destroyed */ g_hash_table_insert(s->cb_shared_files, file, path); } paths[n] = path; n++; } g_object_unref(root); data = strv_concat(paths, size_out); g_free(paths); return data; } static GdkAtom a_gnome, a_mate, a_nautilus, a_uri_list, a_kde_cut; static void init_uris_atoms() { if (a_gnome != GDK_NONE) { return; } a_gnome = gdk_atom_intern_static_string("x-special/gnome-copied-files"); a_mate = gdk_atom_intern_static_string("x-special/mate-copied-files"); a_nautilus = gdk_atom_intern_static_string("UTF8_STRING"); a_uri_list = gdk_atom_intern_static_string("text/uri-list"); a_kde_cut = gdk_atom_intern_static_string("application/x-kde-cutselection"); } static GdkAtom clipboard_select_uris_atom(SpiceGtkSessionPrivate *s, guint selection) { init_uris_atoms(); if (clipboard_find_atom(s, selection, a_gnome)) { return a_gnome; } if (clipboard_find_atom(s, selection, a_mate)) { return a_mate; } if (clipboard_find_atom(s, selection, a_nautilus)) { return a_nautilus; } return clipboard_find_atom(s, selection, a_uri_list); } /* common handler for "x-special/gnome-copied-files" and "x-special/mate-copied-files" */ static gchar *x_special_copied_files_transform_to_data(SpiceGtkSessionPrivate *s, GtkSelectionData *selection_data, gsize *size_out) { const gchar *text; gchar **lines, *data = NULL; GdkDragAction action; *size_out = 0; text = (gchar *)gtk_selection_data_get_data(selection_data); if (!text) { return NULL; } lines = g_strsplit(text, "\n", -1); if (g_strv_length(lines) < 2) { goto err; } if (!g_strcmp0(lines[0], "cut")) { action = GDK_ACTION_MOVE; } else if (!g_strcmp0(lines[0], "copy")) { action = GDK_ACTION_COPY; } else { goto err; } data = strv_uris_transform_to_data(s, &lines[1], size_out, action); err: g_strfreev(lines); return data; } /* used with newer Nautilus */ static gchar *nautilus_uris_transform_to_data(SpiceGtkSessionPrivate *s, GtkSelectionData *selection_data, gsize *size_out, gboolean *retry_out) { gchar **lines, *text, *data = NULL; guint n_lines; GdkDragAction action; *size_out = 0; text = (gchar *)gtk_selection_data_get_text(selection_data); if (!text) { return NULL; } lines = g_strsplit(text, "\n", -1); g_free(text); n_lines = g_strv_length(lines); if (n_lines < 4) { *retry_out = TRUE; goto err; } if (g_strcmp0(lines[0], "x-special/nautilus-clipboard")) { *retry_out = TRUE; goto err; } if (!g_strcmp0(lines[1], "cut")) { action = GDK_ACTION_MOVE; } else if (!g_strcmp0(lines[1], "copy")) { action = GDK_ACTION_COPY; } else { goto err; } /* the list of uris must end with \n, * so there must be an empty string after the split */ if (g_strcmp0(lines[n_lines-1], "")) { goto err; } g_clear_pointer(&lines[n_lines-1], g_free); data = strv_uris_transform_to_data(s, &lines[2], size_out, action); err: g_strfreev(lines); return data; } static GdkDragAction kde_get_clipboard_action(SpiceGtkSessionPrivate *s, GtkClipboard *clipboard) { GtkSelectionData *selection_data; GdkDragAction action; const guchar *data; /* this uses another GMainLoop, basically the same mechanism * as we use in clipboard_get(), so it doesn't block */ selection_data = gtk_clipboard_wait_for_contents(clipboard, a_kde_cut); data = gtk_selection_data_get_data(selection_data); if (data && data[0] == '1') { action = GDK_ACTION_MOVE; } else { action = GDK_ACTION_COPY; } gtk_selection_data_free(selection_data); return action; } static void clipboard_received_uri_contents_cb(GtkClipboard *clipboard, GtkSelectionData *selection_data, gpointer user_data) { SpiceGtkSession *self = free_weak_ref(user_data); SpiceGtkSessionPrivate *s; guint selection; if (!self) { return; } s = self->priv; selection = get_selection_from_clipboard(s, clipboard); g_return_if_fail(selection != -1); init_uris_atoms(); GdkAtom type = gtk_selection_data_get_data_type(selection_data); gchar *data; gsize len; if (type == a_gnome || type == a_mate) { /* used by old Nautilus + many other file managers */ data = x_special_copied_files_transform_to_data(s, selection_data, &len); } else if (type == a_nautilus) { gboolean retry = FALSE; data = nautilus_uris_transform_to_data(s, selection_data, &len, &retry); if (retry && clipboard_find_atom(s, selection, a_uri_list) != GDK_NONE) { /* it's not Nautilus, so we give it one more try with the generic uri-list target */ gtk_clipboard_request_contents(clipboard, a_uri_list, clipboard_received_uri_contents_cb, get_weak_ref(self)); return; } } else if (type == a_uri_list) { GdkDragAction action = GDK_ACTION_COPY; gchar **uris = gtk_selection_data_get_uris(selection_data); /* KDE uses a separate atom to distinguish between copy and move operation */ if (clipboard_find_atom(s, selection, a_kde_cut) != GDK_NONE) { action = kde_get_clipboard_action(s, clipboard); } data = strv_uris_transform_to_data(s, uris, &len, action); g_strfreev(uris); } else { g_warning("received uris in unsupported type"); data = NULL; len = 0; } spice_main_channel_clipboard_selection_notify(s->main, selection, VD_AGENT_CLIPBOARD_FILE_LIST, (guchar *)data, len); g_free(data); } #endif static void clipboard_received_cb(GtkClipboard *clipboard, GtkSelectionData *selection_data, gpointer user_data) { SpiceGtkSession *self = free_weak_ref(user_data); if (self == NULL) return; g_return_if_fail(SPICE_IS_GTK_SESSION(self)); SpiceGtkSessionPrivate *s = self->priv; gint len = 0, m; guint32 type = VD_AGENT_CLIPBOARD_NONE; gchar* name; GdkAtom atom; int selection; selection = get_selection_from_clipboard(s, clipboard); g_return_if_fail(selection != -1); len = gtk_selection_data_get_length(selection_data); if (!check_clipboard_size_limits(self, len)) { return; } else { atom = gtk_selection_data_get_data_type(selection_data); name = gdk_atom_name(atom); for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) { if (strcasecmp(name, atom2agent[m].xatom) == 0) { break; } } if (m >= SPICE_N_ELEMENTS(atom2agent)) { g_warning("clipboard_received for unsupported type: %s", name); } else { type = atom2agent[m].vdagent; } g_free(name); } const guchar *data = gtk_selection_data_get_data(selection_data); /* text should be handled through clipboard_received_text_cb(), not * clipboard_received_cb(). */ g_warn_if_fail(type != VD_AGENT_CLIPBOARD_UTF8_TEXT); spice_main_channel_clipboard_selection_notify(s->main, selection, type, data, len); } static gboolean clipboard_request(SpiceMainChannel *main, guint selection, guint type, gpointer user_data) { g_return_val_if_fail(SPICE_IS_GTK_SESSION(user_data), FALSE); SpiceGtkSession *self = user_data; SpiceGtkSessionPrivate *s = self->priv; GdkAtom atom; GtkClipboard* cb; int m; cb = get_clipboard_from_selection(s, selection); g_return_val_if_fail(cb != NULL, FALSE); g_return_val_if_fail(s->clipboard_by_guest[selection] == FALSE, FALSE); g_return_val_if_fail(s->clip_grabbed[selection], FALSE); if (read_only(self)) return FALSE; if (type == VD_AGENT_CLIPBOARD_UTF8_TEXT) { gtk_clipboard_request_text(cb, clipboard_received_text_cb, get_weak_ref(self)); } else if (type == VD_AGENT_CLIPBOARD_FILE_LIST) { #ifdef HAVE_PHODAV_VIRTUAL atom = clipboard_select_uris_atom(s, selection); if (atom == GDK_NONE) { return FALSE; } gtk_clipboard_request_contents(cb, atom, clipboard_received_uri_contents_cb, get_weak_ref(self)); #else return FALSE; #endif } else { for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) { if (atom2agent[m].vdagent == type) break; } g_return_val_if_fail(m < SPICE_N_ELEMENTS(atom2agent), FALSE); atom = gdk_atom_intern_static_string(atom2agent[m].xatom); gtk_clipboard_request_contents(cb, atom, clipboard_received_cb, get_weak_ref(self)); } return TRUE; } static void clipboard_release(SpiceGtkSession *self, guint selection) { SpiceGtkSessionPrivate *s = self->priv; GtkClipboard* clipboard = get_clipboard_from_selection(s, selection); g_return_if_fail(clipboard != NULL); s->nclip_targets[selection] = 0; if (!s->clipboard_by_guest[selection]) return; gtk_clipboard_clear(clipboard); s->clipboard_by_guest[selection] = FALSE; } typedef struct SpiceGtkClipboardRelease { SpiceGtkSession *self; guint selection; } SpiceGtkClipboardRelease; static gboolean clipboard_release_timeout(gpointer user_data) { SpiceGtkClipboardRelease *rel = user_data; clipboard_release_delay_remove(rel->self, rel->selection, true); return G_SOURCE_REMOVE; } /* * The agents send release between two grabs. This may trigger * clipboard managers trying to grab the clipboard. We end up with two * sides, client and remote, racing for the clipboard grab, and * believing each other is the owner. * * Workaround this problem by delaying the release event by 0.5 sec, * unless the no-release-on-regrab capability is present. */ #define CLIPBOARD_RELEASE_DELAY 500 /* ms */ static void clipboard_release_delay(SpiceMainChannel *main, guint selection, gpointer user_data) { SpiceGtkSession *self = SPICE_GTK_SESSION(user_data); SpiceGtkSessionPrivate *s = self->priv; GtkClipboard* clipboard = get_clipboard_from_selection(s, selection); SpiceGtkClipboardRelease *rel; if (!clipboard) { return; } clipboard_release_delay_remove(self, selection, true); if (spice_main_channel_agent_test_capability(s->main, VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB)) { clipboard_release(self, selection); return; } rel = g_new0(SpiceGtkClipboardRelease, 1); rel->self = self; rel->selection = selection; s->clipboard_release_delay[selection] = g_timeout_add_full(G_PRIORITY_DEFAULT, CLIPBOARD_RELEASE_DELAY, clipboard_release_timeout, rel, g_free); } static void channel_new(SpiceSession *session, SpiceChannel *channel, gpointer user_data) { g_return_if_fail(SPICE_IS_GTK_SESSION(user_data)); SpiceGtkSession *self = user_data; SpiceGtkSessionPrivate *s = self->priv; if (SPICE_IS_MAIN_CHANNEL(channel)) { SPICE_DEBUG("Changing main channel from %p to %p", s->main, channel); s->main = SPICE_MAIN_CHANNEL(channel); g_signal_connect(channel, "main-clipboard-selection-grab", G_CALLBACK(clipboard_grab), self); g_signal_connect(channel, "main-clipboard-selection-request", G_CALLBACK(clipboard_request), self); g_signal_connect(channel, "main-clipboard-selection-release", G_CALLBACK(clipboard_release_delay), self); } if (SPICE_IS_INPUTS_CHANNEL(channel)) { spice_g_signal_connect_object(channel, "inputs-modifiers", G_CALLBACK(guest_modifiers_changed), self, 0); spice_gtk_session_sync_keyboard_modifiers_for_channel(self, SPICE_INPUTS_CHANNEL(channel), TRUE); } } static void channel_destroy(SpiceSession *session, SpiceChannel *channel, gpointer user_data) { g_return_if_fail(SPICE_IS_GTK_SESSION(user_data)); SpiceGtkSession *self = user_data; SpiceGtkSessionPrivate *s = self->priv; guint i; if (SPICE_IS_MAIN_CHANNEL(channel) && SPICE_MAIN_CHANNEL(channel) == s->main) { s->main = NULL; for (i = 0; i < CLIPBOARD_LAST; ++i) { if (s->clipboard_by_guest[i]) { GtkClipboard *cb = get_clipboard_from_selection(s, i); if (cb) gtk_clipboard_clear(cb); s->clipboard_by_guest[i] = FALSE; } s->clip_grabbed[i] = FALSE; s->nclip_targets[i] = 0; } } } static gboolean read_only(SpiceGtkSession *self) { return spice_session_get_read_only(self->priv->session); } /* ---------------------------------------------------------------- */ /* private functions (usbredir related) */ G_GNUC_INTERNAL void spice_gtk_session_request_auto_usbredir(SpiceGtkSession *self, gboolean state) { g_return_if_fail(SPICE_IS_GTK_SESSION(self)); SpiceGtkSessionPrivate *s = self->priv; SpiceDesktopIntegration *desktop_int; SpiceUsbDeviceManager *manager; if (state) { s->auto_usbredir_reqs++; if (s->auto_usbredir_reqs != 1) return; } else { g_return_if_fail(s->auto_usbredir_reqs > 0); s->auto_usbredir_reqs--; if (s->auto_usbredir_reqs != 0) return; } if (!s->auto_usbredir_enable) return; manager = spice_usb_device_manager_get(s->session, NULL); if (!manager) return; g_object_set(manager, "auto-connect", state, NULL); desktop_int = spice_desktop_integration_get(s->session); if (state) spice_desktop_integration_inhibit_automount(desktop_int); else spice_desktop_integration_uninhibit_automount(desktop_int); } /* ------------------------------------------------------------------ */ /* public functions */ /** * spice_gtk_session_get: * @session: #SpiceSession for which to get the #SpiceGtkSession * * Gets the #SpiceGtkSession associated with the passed in #SpiceSession. * A new #SpiceGtkSession instance will be created the first time this * function is called for a certain #SpiceSession. * * Note that this function returns a weak reference, which should not be used * after the #SpiceSession itself has been unref-ed by the caller. * * Returns: (transfer none): a weak reference to the #SpiceGtkSession associated with the passed in #SpiceSession * * Since 0.8 **/ SpiceGtkSession *spice_gtk_session_get(SpiceSession *session) { g_return_val_if_fail(SPICE_IS_SESSION(session), NULL); SpiceGtkSession *self; static GMutex mutex; g_mutex_lock(&mutex); self = g_object_get_data(G_OBJECT(session), "spice-gtk-session"); if (self == NULL) { self = g_object_new(SPICE_TYPE_GTK_SESSION, "session", session, NULL); g_object_set_data_full(G_OBJECT(session), "spice-gtk-session", self, g_object_unref); } g_mutex_unlock(&mutex); return SPICE_GTK_SESSION(self); } /** * spice_gtk_session_copy_to_guest: * @self: #SpiceGtkSession * * Copy client-side clipboard to guest clipboard. * * Since 0.8 **/ void spice_gtk_session_copy_to_guest(SpiceGtkSession *self) { g_return_if_fail(SPICE_IS_GTK_SESSION(self)); g_return_if_fail(read_only(self) == FALSE); SpiceGtkSessionPrivate *s = self->priv; int selection = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; if (s->clip_hasdata[selection] && !s->clip_grabbed[selection]) { gtk_clipboard_request_targets(s->clipboard, clipboard_get_targets, get_weak_ref(self)); } } /** * spice_gtk_session_paste_from_guest: * @self: #SpiceGtkSession * * Copy guest clipboard to client-side clipboard. * * Since 0.8 **/ void spice_gtk_session_paste_from_guest(SpiceGtkSession *self) { g_return_if_fail(SPICE_IS_GTK_SESSION(self)); g_return_if_fail(read_only(self) == FALSE); SpiceGtkSessionPrivate *s = self->priv; int selection = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD; if (s->nclip_targets[selection] == 0) { g_warning("Guest clipboard is not available."); return; } if (!gtk_clipboard_set_with_owner(s->clipboard, s->clip_targets[selection], s->nclip_targets[selection], clipboard_get, clipboard_clear, G_OBJECT(self))) { g_warning("Clipboard grab failed"); return; } s->clipboard_by_guest[selection] = TRUE; s->clip_hasdata[selection] = FALSE; } G_GNUC_INTERNAL void spice_gtk_session_sync_keyboard_modifiers(SpiceGtkSession *self) { GList *l = NULL, *channels = spice_session_get_channels(self->priv->session); for (l = channels; l != NULL; l = l->next) { if (SPICE_IS_INPUTS_CHANNEL(l->data)) { SpiceInputsChannel *inputs = SPICE_INPUTS_CHANNEL(l->data); spice_gtk_session_sync_keyboard_modifiers_for_channel(self, inputs, TRUE); } } g_list_free(channels); } G_GNUC_INTERNAL void spice_gtk_session_set_pointer_grabbed(SpiceGtkSession *self, gboolean grabbed) { g_return_if_fail(SPICE_IS_GTK_SESSION(self)); self->priv->pointer_grabbed = grabbed; g_object_notify(G_OBJECT(self), "pointer-grabbed"); } G_GNUC_INTERNAL gboolean spice_gtk_session_get_pointer_grabbed(SpiceGtkSession *self) { g_return_val_if_fail(SPICE_IS_GTK_SESSION(self), FALSE); return self->priv->pointer_grabbed; } G_GNUC_INTERNAL void spice_gtk_session_set_keyboard_has_focus(SpiceGtkSession *self, gboolean keyboard_has_focus) { g_return_if_fail(SPICE_IS_GTK_SESSION(self)); self->priv->keyboard_has_focus = keyboard_has_focus; } G_GNUC_INTERNAL void spice_gtk_session_set_mouse_has_pointer(SpiceGtkSession *self, gboolean mouse_has_pointer) { g_return_if_fail(SPICE_IS_GTK_SESSION(self)); self->priv->mouse_has_pointer = mouse_has_pointer; } G_GNUC_INTERNAL gboolean spice_gtk_session_get_keyboard_has_focus(SpiceGtkSession *self) { g_return_val_if_fail(SPICE_IS_GTK_SESSION(self), FALSE); return self->priv->keyboard_has_focus; } G_GNUC_INTERNAL gboolean spice_gtk_session_get_mouse_has_pointer(SpiceGtkSession *self) { g_return_val_if_fail(SPICE_IS_GTK_SESSION(self), FALSE); return self->priv->mouse_has_pointer; }