/*
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;
}