diff options
author | Ondrej Holy <oholy@redhat.com> | 2015-09-30 18:00:29 +0200 |
---|---|---|
committer | Ondrej Holy <oholy@redhat.com> | 2015-11-04 16:46:32 +0100 |
commit | 5cd0129bf2aad3b9eec70ec8303439c95f60d619 (patch) | |
tree | 778fbddee4e72fa2063f845f669367953dc16062 | |
parent | 75253b7a8fb8fa59c317abeb01b4fa3981597d9e (diff) |
Add initial seamless mode supportseamless-mode
Seamless mode is a way to use guest applications directly on the
client system desktop side-by-side with client applications.
Add toggle button to enable/disable seamless mode in spicy. If
seamless mode is enabled, client receive list of visible areas
periodicaly. Spice widget draw only visible areas (rest is
transparent). Spicy hide window decorations and show only
maximized spice widget. Spicy also set window shape to match
visible areas. Window shape is slightly bigger then visible areas
to avoid losing focus when resizing.
https://bugs.freedesktop.org/show_bug.cgi?id=39238
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/channel-main.c | 129 | ||||
-rw-r--r-- | src/channel-main.h | 4 | ||||
-rw-r--r-- | src/map-file | 2 | ||||
-rw-r--r-- | src/spice-glib-sym-file | 2 | ||||
-rw-r--r-- | src/spice-session.c | 1 | ||||
-rw-r--r-- | src/spice-widget-cairo.c | 17 | ||||
-rw-r--r-- | src/spicy.c | 153 |
8 files changed, 308 insertions, 2 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index cf02198..02090fd 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -646,7 +646,7 @@ gtk_introspection_files = \ $(NULL) SpiceClientGLib-2.0.gir: libspice-client-glib-2.0.la -SpiceClientGLib_2_0_gir_INCLUDES = GObject-2.0 Gio-2.0 +SpiceClientGLib_2_0_gir_INCLUDES = GObject-2.0 Gio-2.0 Gdk-3.0 SpiceClientGLib_2_0_gir_CFLAGS = $(SPICE_COMMON_CPPFLAGS) SpiceClientGLib_2_0_gir_LIBS = libspice-client-glib-2.0.la SpiceClientGLib_2_0_gir_FILES = $(glib_introspection_files) diff --git a/src/channel-main.c b/src/channel-main.c index 2af9e64..4361c86 100644 --- a/src/channel-main.c +++ b/src/channel-main.c @@ -21,6 +21,7 @@ #include <spice/vd_agent.h> #include <common/rect.h> #include <glib/gstdio.h> +#include <gdk/gdk.h> #include "glib-compat.h" #include "spice-client.h" @@ -121,6 +122,10 @@ struct _SpiceMainChannelPrivate { gboolean agent_volume_playback_sync; gboolean agent_volume_record_sync; GCancellable *cancellable_volume_info; + + GMutex seamless_mode_lock; + GList *seamless_mode_list; + gboolean seamless_mode; }; struct spice_migrate { @@ -153,6 +158,8 @@ enum { PROP_DISABLE_DISPLAY_POSITION, PROP_DISABLE_DISPLAY_ALIGN, PROP_MAX_CLIPBOARD, + PROP_SEAMLESS_MODE, + PROP_SEAMLESS_MODE_LIST, }; /* Signals */ @@ -200,6 +207,8 @@ static const char *agent_msg_types[] = { [ VD_AGENT_CLIPBOARD_REQUEST ] = "clipboard request", [ VD_AGENT_CLIPBOARD_RELEASE ] = "clipboard release", [ VD_AGENT_AUDIO_VOLUME_SYNC ] = "volume-sync", + [ VD_AGENT_SEAMLESS_MODE ] = "seamless mode", + [ VD_AGENT_SEAMLESS_MODE_LIST ] = "seamless mode list", }; static const char *agent_caps[] = { @@ -215,6 +224,7 @@ static const char *agent_caps[] = { [ VD_AGENT_CAP_GUEST_LINEEND_CRLF ] = "line-end crlf", [ VD_AGENT_CAP_MAX_CLIPBOARD ] = "max-clipboard", [ VD_AGENT_CAP_AUDIO_VOLUME_SYNC ] = "volume-sync", + [ VD_AGENT_CAP_SEAMLESS_MODE ] = "seamless mode", [ VD_AGENT_CAP_MONITORS_CONFIG_POSITION ] = "monitors config position", }; #define NAME(_a, _i) ((_i) < SPICE_N_ELEMENTS(_a) ? (_a[(_i)] ?: "?") : "?") @@ -247,6 +257,7 @@ static void spice_main_channel_init(SpiceMainChannel *channel) c->agent_msg_queue = g_queue_new(); c->file_xfer_tasks = g_hash_table_new(g_direct_hash, g_direct_equal); c->cancellable_volume_info = g_cancellable_new(); + g_mutex_init(&c->seamless_mode_lock); spice_main_channel_reset_capabilties(SPICE_CHANNEL(channel)); } @@ -300,6 +311,12 @@ static void spice_main_get_property(GObject *object, case PROP_MAX_CLIPBOARD: g_value_set_int(value, spice_main_get_max_clipboard(self)); break; + case PROP_SEAMLESS_MODE: + g_value_set_boolean(value, c->seamless_mode); + break; + case PROP_SEAMLESS_MODE_LIST: + g_value_set_pointer(value, spice_main_get_seamless_mode_list(self)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; @@ -337,6 +354,9 @@ static void spice_main_set_property(GObject *gobject, guint prop_id, case PROP_MAX_CLIPBOARD: spice_main_set_max_clipboard(self, g_value_get_int(value)); break; + case PROP_SEAMLESS_MODE: + spice_main_set_seamless_mode(self, g_value_get_boolean(value)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec); break; @@ -365,6 +385,10 @@ static void spice_main_channel_dispose(GObject *obj) g_cancellable_cancel(c->cancellable_volume_info); g_clear_object(&c->cancellable_volume_info); + g_mutex_clear(&c->seamless_mode_lock); + g_list_free_full(c->seamless_mode_list, g_free); + c->seamless_mode_list = NULL; + if (G_OBJECT_CLASS(spice_main_channel_parent_class)->dispose) G_OBJECT_CLASS(spice_main_channel_parent_class)->dispose(obj); } @@ -597,6 +621,23 @@ static void spice_main_channel_class_init(SpiceMainChannelClass *klass) G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property + (gobject_class, PROP_SEAMLESS_MODE, + g_param_spec_boolean("seamless-mode", + "Seamless mode", + "Seamless mode", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property + (gobject_class, PROP_SEAMLESS_MODE_LIST, + g_param_spec_pointer ("seamless-mode-list", + "Seamless mode list", + "Seamless mode window list", + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + /* TODO use notify instead */ /** * SpiceMainChannel::main-mouse-update: @@ -1282,6 +1323,7 @@ static void agent_announce_caps(SpiceMainChannel *channel) VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_DISPLAY_CONFIG); VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND); VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_SELECTION); + VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_SEAMLESS_MODE); VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_MONITORS_CONFIG_POSITION); agent_msg_queue(channel, VD_AGENT_ANNOUNCE_CAPABILITIES, size, caps); @@ -2054,6 +2096,33 @@ static void main_agent_handle_msg(SpiceChannel *channel, case VD_AGENT_FILE_XFER_STATUS: file_xfer_handle_status(self, payload); break; + case VD_AGENT_SEAMLESS_MODE_LIST: + { + VDAgentSeamlessModeList *list = payload; + int i; + + g_mutex_lock(&self->priv->seamless_mode_lock); + + g_list_free_full(c->seamless_mode_list, g_free); + c->seamless_mode_list = NULL; + + for (i = 0; i < list->num_of_windows; i++) { + GdkRectangle *r; + + r = g_new0(GdkRectangle, 1); + r->x = list->windows[i].x; + r->y = list->windows[i].y; + r->width = list->windows[i].w; + r->height = list->windows[i].h; + + c->seamless_mode_list = g_list_append(c->seamless_mode_list, r); + } + + g_mutex_unlock(&self->priv->seamless_mode_lock); + + g_coroutine_object_notify(G_OBJECT(self), "seamless-mode-list"); + break; + } default: g_warning("unhandled agent message type: %u (%s), size %u", msg->type, NAME(agent_msg_types, msg->type), msg->size); @@ -3088,3 +3157,63 @@ gboolean spice_main_file_copy_finish(SpiceMainChannel *channel, return g_simple_async_result_get_op_res_gboolean(simple); } + +/** + * spice_main_set_seamless_mode: + * @channel: a #SpiceMainChannel + * @enabled: whether seamless mode is enabled + * + * Sent message to agent to enable/disable seamless mode list updates. + **/ +void spice_main_set_seamless_mode(SpiceMainChannel *channel, + gboolean enabled) +{ + SpiceMainChannelPrivate *c = channel->priv; + VDAgentSeamlessMode msg; + + g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel)); + g_return_if_fail(spice_main_get_seamless_mode_supported(channel)); + + if (c->seamless_mode == enabled) + return; + + c->seamless_mode = msg.enabled = enabled; + agent_msg_queue(channel, VD_AGENT_SEAMLESS_MODE, sizeof(msg), &msg); + spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE); +} + +/** + * spice_main_get_seamless_mode_supported: + * @channel: a #SpiceMainChannel + * + * Returns: %TRUE if seamless mode is supported by the agent. + **/ +gboolean spice_main_get_seamless_mode_supported(SpiceMainChannel *channel) +{ + g_return_val_if_fail(SPICE_IS_MAIN_CHANNEL(channel), FALSE); + + return test_agent_cap(channel, VD_AGENT_CAP_SEAMLESS_MODE); +} + +/** + * spice_main_get_seamless_mode_list: + * @channel: a #SpiceMainChannel + * + * Seamless mode has to be enabled using spice_main_set_seamless_mode() to get + * updated list of visible areas. + * + * Returns: (transfer full) (element-type GdkRectangle): a newly allocated + * list of GdkRectangle structs. + **/ +GList *spice_main_get_seamless_mode_list(SpiceMainChannel *channel) +{ + GList *list; + + g_mutex_lock(&channel->priv->seamless_mode_lock); + list = g_list_copy_deep(channel->priv->seamless_mode_list, + (GCopyFunc)g_memdup, + (gpointer)sizeof(GdkRectangle)); + g_mutex_unlock(&channel->priv->seamless_mode_lock); + + return list; +} diff --git a/src/channel-main.h b/src/channel-main.h index 86bb46b..1a083bc 100644 --- a/src/channel-main.h +++ b/src/channel-main.h @@ -105,6 +105,10 @@ SPICE_DEPRECATED_FOR(spice_main_clipboard_selection_request) void spice_main_clipboard_request(SpiceMainChannel *channel, guint32 type); #endif +void spice_main_set_seamless_mode(SpiceMainChannel *channel, gboolean enabled); +gboolean spice_main_get_seamless_mode_supported(SpiceMainChannel *channel); +GList *spice_main_get_seamless_mode_list(SpiceMainChannel *channel); + G_END_DECLS #endif /* __SPICE_CLIENT_MAIN_CHANNEL_H__ */ diff --git a/src/map-file b/src/map-file index a9abc61..1692ff3 100644 --- a/src/map-file +++ b/src/map-file @@ -67,6 +67,8 @@ spice_main_clipboard_selection_release; spice_main_clipboard_selection_request; spice_main_file_copy_async; spice_main_file_copy_finish; +spice_main_get_seamless_mode_list; +spice_main_get_seamless_mode_supported; spice_main_send_monitor_config; spice_main_set_display; spice_main_set_display_enabled; diff --git a/src/spice-glib-sym-file b/src/spice-glib-sym-file index 1d62716..f2f0591 100644 --- a/src/spice-glib-sym-file +++ b/src/spice-glib-sym-file @@ -44,6 +44,8 @@ spice_main_clipboard_selection_release spice_main_clipboard_selection_request spice_main_file_copy_async spice_main_file_copy_finish +spice_main_get_seamless_mode_list +spice_main_get_seamless_mode_supported spice_main_send_monitor_config spice_main_set_display spice_main_set_display_enabled diff --git a/src/spice-session.c b/src/spice-session.c index e1d9997..ce62d0a 100644 --- a/src/spice-session.c +++ b/src/spice-session.c @@ -22,6 +22,7 @@ #ifdef G_OS_UNIX #include <gio/gunixsocketaddress.h> #endif +#include <spice/vd_agent.h> #include "common/ring.h" #include "spice-client.h" diff --git a/src/spice-widget-cairo.c b/src/spice-widget-cairo.c index 96af076..6f3cf16 100644 --- a/src/spice-widget-cairo.c +++ b/src/spice-widget-cairo.c @@ -104,12 +104,27 @@ void spicex_draw_event(SpiceDisplay *display, cairo_t *cr) /* Need to set a real solid color, because the default is usually transparent these days, and non-double buffered windows can't render transparently */ - cairo_set_source_rgb (cr, 0, 0, 0); + cairo_set_source_rgba (cr, 0, 0, 0, 0); cairo_fill(cr); /* Draw the display */ if (d->ximage) { + gboolean seamless_mode; + GList *list, *l; + cairo_translate(cr, x, y); + + list = spice_main_get_seamless_mode_list(d->main); + g_object_get(d->main, "seamless-mode", &seamless_mode, NULL); + if (seamless_mode && list) { + for (l = list; l != NULL; l = l->next) { + GdkRectangle *r = l->data; + cairo_rectangle(cr, r->x, r->y, r->width, r->height); + } + cairo_clip(cr); + } + g_list_free_full(list, g_free); + cairo_rectangle(cr, 0, 0, w, h); cairo_scale(cr, s, s); if (!d->convert) diff --git a/src/spicy.c b/src/spicy.c index f48a220..c7c5c88 100644 --- a/src/spicy.c +++ b/src/spicy.c @@ -71,6 +71,7 @@ struct _SpiceWindow { GtkActionGroup *ag; GtkUIManager *ui; bool fullscreen; + bool seamless_mode; bool mouse_grabbed; SpiceChannel *display_channel; #ifdef G_OS_WIN32 @@ -90,6 +91,7 @@ G_DEFINE_TYPE (SpiceWindow, spice_window, G_TYPE_OBJECT); #define CHANNELID_MAX 4 #define MONITORID_MAX 4 +#define SEAMLESS_MODE_BORDER 50 // FIXME: turn this into an object, get rid of fixed wins array, use // signals to replace the various callback that iterate over wins array @@ -116,6 +118,7 @@ static void usb_connect_failed(GObject *object, gpointer data); static gboolean is_gtk_session_property(const gchar *property); static void del_window(spice_connection *conn, SpiceWindow *win); +static void window_set_seamless_mode(SpiceWindow *win, gboolean enabled); /* options */ static gboolean fullscreen = false; @@ -216,6 +219,7 @@ static void update_edit_menu_window(SpiceWindow *win) { int i; GtkAction *toggle; + gboolean state; if (win == NULL) { return; @@ -229,6 +233,14 @@ static void update_edit_menu_window(SpiceWindow *win) gtk_action_set_sensitive(toggle, win->conn->agent_connected); } } + + toggle = gtk_action_group_get_action(win->ag, "SeamlessMode"); + state = spice_main_get_seamless_mode_supported(win->conn->main) && + win->conn->agent_connected; + gtk_action_set_sensitive(toggle, state); + + if (!state && win->seamless_mode) + window_set_seamless_mode(win, FALSE); } static void update_edit_menu(struct spice_connection *conn) @@ -293,6 +305,129 @@ static void menu_cb_fullscreen(GtkAction *action, void *data) window_set_fullscreen(win, !win->fullscreen); } +static gboolean draw_cb(GtkWidget *widget, GdkEventExpose *event, gpointer userdata) +{ + cairo_t *cr; + + cr = gdk_cairo_create(gtk_widget_get_window(widget)); + + cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.0); + + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_paint(cr); + + cairo_destroy(cr); + + return FALSE; +} + +static void update_seamless_mode_window(SpiceWindow *win) +{ + GList *list, *l; + cairo_region_t *region = NULL; + + list = spice_main_get_seamless_mode_list(win->conn->main); + if (win->seamless_mode && list) { + region = cairo_region_create(); + for (l = list; l != NULL; l = l->next) { + GdkRectangle rectangle, *r; + + r = (GdkRectangle *)l->data; + rectangle.x = r->x - SEAMLESS_MODE_BORDER; + rectangle.y = r->y - SEAMLESS_MODE_BORDER; + rectangle.width = r->width + 2 * SEAMLESS_MODE_BORDER; + rectangle.height = r->height + 2 * SEAMLESS_MODE_BORDER; + + cairo_region_union_rectangle(region, &rectangle); + } + } + g_list_free_full(list, g_free); + + gdk_window_shape_combine_region(gtk_widget_get_window(win->toplevel), region, 0, 0); +} + +static void main_seamless_mode_update(SpiceChannel *channel, + GParamSpec *pspec, + SpiceWindow *win) +{ + update_seamless_mode_window(win); +} + +static void window_set_seamless_mode(SpiceWindow *win, gboolean enabled) +{ + gboolean state; + GError *error = NULL; + + win->seamless_mode = enabled; + g_object_set(win->conn->main, "seamless-mode", win->seamless_mode, NULL); + update_seamless_mode_window(win); + + gtk_window_set_decorated(GTK_WINDOW(win->toplevel), !win->seamless_mode); + gtk_widget_set_visible(win->menubar, !win->seamless_mode); + gtk_widget_set_app_paintable(win->toplevel, win->seamless_mode); + + if (win->seamless_mode) { + gtk_window_maximize(GTK_WINDOW(win->toplevel)); + + gtk_widget_set_visible(win->toolbar, FALSE); + gtk_widget_set_visible(win->statusbar, FALSE); + + g_object_set (win->spice, "grab-keyboard", FALSE, NULL); + g_object_set (win->spice, "resize-guest", TRUE, NULL); + g_object_set (win->spice, "scaling", FALSE, NULL); + + g_signal_connect(G_OBJECT(win->toplevel), "draw", + G_CALLBACK(draw_cb), NULL); + + g_signal_connect(win->conn->main, "notify::seamless-mode-list", + G_CALLBACK(main_seamless_mode_update), win); + update_seamless_mode_window(win); + + gtk_window_present(GTK_WINDOW(win->toplevel)); + } else { + // TODO: Restore window geometry properly + gtk_window_unmaximize(GTK_WINDOW(win->toplevel)); + + state = g_key_file_get_boolean(keyfile, "ui", "toolbar", &error); + gtk_widget_set_visible(win->toolbar, error ? TRUE : state); + g_clear_error (&error); + + state = g_key_file_get_boolean(keyfile, "ui", "statusbar", &error); + gtk_widget_set_visible(win->statusbar, error ? TRUE : state); + g_clear_error (&error); + + state = g_key_file_get_boolean(keyfile, "general", "grab-keyboard", &error); + if (!error) + g_object_set (win->spice, "grab-keyboard", state, NULL); + g_clear_error (&error); + + state = g_key_file_get_boolean(keyfile, "general", "resize-guest", &error); + if (!error) + g_object_set (win->spice, "resize-guest", state, NULL); + g_clear_error (&error); + + state = g_key_file_get_boolean(keyfile, "general", "scaling", &error); + if (!error) + g_object_set (win->spice, "scaling", state, NULL); + g_clear_error (&error); + + g_signal_handlers_disconnect_by_func(win->conn->main, + G_CALLBACK(draw_cb), + NULL); + + g_signal_handlers_disconnect_by_func(win->conn->main, + G_CALLBACK(main_seamless_mode_update), + win); + } +} + +static void menu_cb_seamless_mode(GtkAction *action, void *data) +{ + SpiceWindow *win = data; + + window_set_seamless_mode (win, !win->seamless_mode); +} + #ifdef USE_SMARTCARD static void enable_smartcard_actions(SpiceWindow *win, VReader *reader, gboolean can_insert, gboolean can_remove) @@ -666,6 +801,12 @@ static const GtkActionEntry entries[] = { .callback = G_CALLBACK(menu_cb_fullscreen), .accelerator = "<shift>F11", },{ + .name = "SeamlessMode", + .label = "_Seamless mode", + .callback = G_CALLBACK(menu_cb_seamless_mode), + .accelerator = "<control><shift>S", + },{ + #ifdef USE_SMARTCARD .name = "InsertSmartcard", .label = "_Insert Smartcard", @@ -763,6 +904,7 @@ static char ui_xml[] = " </menu>\n" " <menu action='ViewMenu'>\n" " <menuitem action='Fullscreen'/>\n" +" <menuitem action='SeamlessMode'/>\n" " <menuitem action='Toolbar'/>\n" " <menuitem action='Statusbar'/>\n" " </menu>\n" @@ -795,6 +937,7 @@ static char ui_xml[] = " <toolitem action='PasteFromGuest'/>\n" " <separator/>\n" " <toolitem action='Fullscreen'/>\n" +" <toolitem action='SeamlessMode'/>\n" " </toolbar>\n" "</ui>\n"; @@ -864,6 +1007,8 @@ static SpiceWindow *create_spice_window(spice_connection *conn, SpiceChannel *ch GError *err = NULL; int i; SpiceGrabSequence *seq; + GdkScreen *screen; + GdkVisual *visual; win = g_object_new(SPICE_TYPE_WINDOW, NULL); win->id = id; @@ -879,6 +1024,14 @@ static SpiceWindow *create_spice_window(spice_connection *conn, SpiceChannel *ch snprintf(title, sizeof(title), "%s", spicy_title); } + screen = gtk_widget_get_screen(win->toplevel); + visual = gdk_screen_get_rgba_visual(screen); + if (visual) + gtk_widget_set_visual(win->toplevel, visual); + else + // TODO: Hide seamless mode toggle + g_warning ("Transparency is not supported!"); + gtk_window_set_title(GTK_WINDOW(win->toplevel), title); g_signal_connect(G_OBJECT(win->toplevel), "window-state-event", G_CALLBACK(window_state_cb), win); |