Usage is simply "remote-viewer --spice-controller" --- configure.ac | 3 +- src/remote-viewer-main.c | 20 ++- src/remote-viewer.c | 374 ++++++++++++++++++++++++++++++++++++-- src/remote-viewer.h | 4 +- src/virt-viewer-app.c | 7 + src/virt-viewer-app.h | 1 + src/virt-viewer-session-spice.c | 47 +++++- src/virt-viewer-window.c | 9 + src/virt-viewer-window.h | 1 + 9 files changed, 435 insertions(+), 31 deletions(-) diff --git a/configure.ac b/configure.ac index e47d60a..b2d7e8f 100644 --- a/configure.ac +++ b/configure.ac @@ -92,7 +92,8 @@ AC_ARG_WITH([spice-gtk], AS_IF([test "x$with_spice_gtk" != "xno"], [PKG_CHECK_MODULES(SPICE_GTK, - spice-client-gtk-$SPICE_GTK_API_VERSION >= $SPICE_GTK_REQUIRED, + [spice-client-gtk-$SPICE_GTK_API_VERSION >= $SPICE_GTK_REQUIRED + spice-controller], [have_spice_gtk=yes], [have_spice_gtk=no])], [have_spice_gtk=no]) diff --git a/src/remote-viewer-main.c b/src/remote-viewer-main.c index 54670d1..2256528 100644 --- a/src/remote-viewer-main.c +++ b/src/remote-viewer-main.c @@ -56,6 +56,7 @@ main(int argc, char **argv) gboolean direct = FALSE; gboolean fullscreen = FALSE; RemoteViewer *viewer = NULL; + gboolean controller = FALSE; VirtViewerApp *app; const char *help_msg = N_("Run '" PACKAGE " --help' to see a full list of available command line options"); const GOptionEntry options [] = { @@ -71,6 +72,8 @@ main(int argc, char **argv) N_("Display debugging information"), NULL }, { "full-screen", 'f', 0, G_OPTION_ARG_NONE, &fullscreen, N_("Open in full screen mode"), NULL }, + { "spice-controller", '\0', 0, G_OPTION_ARG_NONE, &controller, + N_("Open connection using Spice controller communication"), NULL }, { G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_STRING_ARRAY, &args, NULL, "URI" }, { NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL } @@ -99,7 +102,8 @@ main(int argc, char **argv) g_option_context_free(context); - if (!args || (g_strv_length(args) != 1)) { + if ((!args || (g_strv_length(args) != 1)) && + !controller) { g_printerr(_("\nUsage: %s [OPTIONS] URI\n\n%s\n\n"), argv[0], help_msg); goto cleanup; } @@ -111,15 +115,19 @@ main(int argc, char **argv) virt_viewer_app_set_debug(debug); - viewer = remote_viewer_new(args[0], verbose); + if (controller) { + viewer = remote_viewer_new_with_controller(verbose); + g_object_set(viewer, "guest-name", "defined by Spice controller", NULL); + } else { + viewer = remote_viewer_new(args[0], verbose); + g_object_set(viewer, "guest-name", args[0], NULL); + + } if (viewer == NULL) goto cleanup; app = VIRT_VIEWER_APP(viewer); - g_object_set(app, - "fullscreen", fullscreen, - "guest-name", args[0], - NULL); + g_object_set(app, "fullscreen", fullscreen, NULL); virt_viewer_window_set_zoom_level(virt_viewer_app_get_main_window(app), zoom); virt_viewer_app_set_direct(app, direct); diff --git a/src/remote-viewer.c b/src/remote-viewer.c index d5c9824..388531b 100644 --- a/src/remote-viewer.c +++ b/src/remote-viewer.c @@ -27,24 +27,41 @@ #include <glib/gprintf.h> #include <glib/gi18n.h> +#include <spice-controller/spice-controller.h> + +#include "virt-viewer-session-spice.h" #include "virt-viewer-app.h" #include "remote-viewer.h" struct _RemoteViewerPrivate { - int _dummy; + SpiceCtrlController *controller; + GtkWidget *controller_menu; }; G_DEFINE_TYPE (RemoteViewer, remote_viewer, VIRT_VIEWER_TYPE_APP) #define GET_PRIVATE(o) \ (G_TYPE_INSTANCE_GET_PRIVATE ((o), REMOTE_VIEWER_TYPE, RemoteViewerPrivate)) +enum { + PROP_0, + PROP_CONTROLLER, +}; + static gboolean remote_viewer_start(VirtViewerApp *self); +static int remote_viewer_activate(VirtViewerApp *self); +static void remote_viewer_window_added(VirtViewerApp *self, VirtViewerWindow *win); static void remote_viewer_get_property (GObject *object, guint property_id, - GValue *value G_GNUC_UNUSED, GParamSpec *pspec) + GValue *value, GParamSpec *pspec) { + RemoteViewer *self = REMOTE_VIEWER(object); + RemoteViewerPrivate *priv = self->priv; + switch (property_id) { + case PROP_CONTROLLER: + g_value_set_object(value, priv->controller); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -52,9 +69,16 @@ remote_viewer_get_property (GObject *object, guint property_id, static void remote_viewer_set_property (GObject *object, guint property_id, - const GValue *value G_GNUC_UNUSED, GParamSpec *pspec) + const GValue *value, GParamSpec *pspec) { + RemoteViewer *self = REMOTE_VIEWER(object); + RemoteViewerPrivate *priv = self->priv; + switch (property_id) { + case PROP_CONTROLLER: + g_return_if_fail(priv->controller == NULL); + priv->controller = g_value_dup_object(value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -63,6 +87,14 @@ remote_viewer_set_property (GObject *object, guint property_id, static void remote_viewer_dispose (GObject *object) { + RemoteViewer *self = REMOTE_VIEWER(object); + RemoteViewerPrivate *priv = self->priv; + + if (priv->controller) { + g_object_unref(priv->controller); + priv->controller = NULL; + } + G_OBJECT_CLASS(remote_viewer_parent_class)->dispose (object); } @@ -79,6 +111,18 @@ remote_viewer_class_init (RemoteViewerClass *klass) object_class->dispose = remote_viewer_dispose; app_class->start = remote_viewer_start; + app_class->activate = remote_viewer_activate; + app_class->window_added = remote_viewer_window_added; + + g_object_class_install_property(object_class, + PROP_CONTROLLER, + g_param_spec_object("controller", + "Controller", + "Spice controller", + SPICE_CTRL_TYPE_CONTROLLER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); } static void @@ -96,36 +140,326 @@ remote_viewer_new(const gchar *uri, gboolean verbose) NULL); } +RemoteViewer * +remote_viewer_new_with_controller(gboolean verbose) +{ + RemoteViewer *self; + SpiceCtrlController *ctrl = spice_ctrl_controller_new(); + + self = g_object_new(REMOTE_VIEWER_TYPE, + "controller", ctrl, + "verbose", verbose, + NULL); + g_object_unref(ctrl); + + return self; +} + +static void +spice_ctrl_do_connect(SpiceCtrlController *ctrl G_GNUC_UNUSED, + VirtViewerApp *self) +{ + if (virt_viewer_app_initial_connect(self) < 0) { + virt_viewer_app_simple_message_dialog(self, _("Failed to initiate connection")); + } +} + +static void +spice_ctrl_show(SpiceCtrlController *ctrl G_GNUC_UNUSED, RemoteViewer *self) +{ + virt_viewer_app_show_display(VIRT_VIEWER_APP(self)); +} + +static void +spice_ctrl_hide(SpiceCtrlController *ctrl G_GNUC_UNUSED, RemoteViewer *self) +{ + virt_viewer_app_show_status(VIRT_VIEWER_APP(self), _("Display disabled by controller")); +} + +static void +spice_menuitem_activate_cb(GtkMenuItem *mi, RemoteViewer *self) +{ + SpiceCtrlMenuItem *menuitem = g_object_get_data(G_OBJECT(mi), "spice-menuitem"); + + g_return_if_fail(menuitem != NULL); + if (gtk_menu_item_get_submenu(mi)) + return; + + spice_ctrl_controller_menu_item_click_msg(self->priv->controller, menuitem->id); +} + +static GtkWidget * +ctrlmenu_to_gtkmenu (RemoteViewer *self, SpiceCtrlMenu *ctrlmenu) +{ + GList *l; + GtkWidget *menu = gtk_menu_new(); + guint n = 0; + + for (l = ctrlmenu->items; l != NULL; l = l->next) { + SpiceCtrlMenuItem *menuitem = l->data; + GtkWidget *item; + char *s; + if (menuitem->text == NULL) { + g_warn_if_reached(); + continue; + } + + for (s = menuitem->text; *s; s++) + if (*s == '&') + *s = '_'; + + if (g_str_equal(menuitem->text, "-")){ + item = gtk_separator_menu_item_new(); + } else { + item = gtk_menu_item_new_with_mnemonic(menuitem->text); + } + + g_object_set_data_full(G_OBJECT(item), "spice-menuitem", + g_object_ref(menuitem), g_object_unref); + g_signal_connect(item, "activate", G_CALLBACK(spice_menuitem_activate_cb), self); + gtk_menu_attach(GTK_MENU (menu), item, 0, 1, n, n + 1); + n += 1; + + if (menuitem->submenu) { + gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), + ctrlmenu_to_gtkmenu(self, menuitem->submenu)); + } + } + + if (n == 0) { + g_object_ref_sink(menu); + g_object_unref(menu); + menu = NULL; + } + + gtk_widget_show_all(menu); + return menu; +} + +static void +spice_menu_set_visible(gpointer key G_GNUC_UNUSED, + gpointer value, + gpointer user_data) +{ + gboolean visible = GPOINTER_TO_INT(user_data); + GtkWidget *menu = g_object_get_data(value, "spice-menu"); + + gtk_widget_set_visible(menu, visible); +} + +static void +remote_viewer_window_spice_menu_set_visible(RemoteViewer *self, + gboolean visible) +{ + GHashTable *windows = virt_viewer_app_get_windows(VIRT_VIEWER_APP(self)); + + g_hash_table_foreach(windows, spice_menu_set_visible, GINT_TO_POINTER(visible)); +} + +static void +spice_menu_update(gpointer key G_GNUC_UNUSED, + gpointer value, + gpointer user_data) +{ + RemoteViewer *self = REMOTE_VIEWER(user_data); + GtkWidget *menuitem = g_object_get_data(value, "spice-menu"); + SpiceCtrlMenu *menu; + + g_object_get(self->priv->controller, "menu", &menu, NULL); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), ctrlmenu_to_gtkmenu(self, menu)); + g_object_unref(menu); +} + +static void +spice_ctrl_menu_updated(RemoteViewer *self, + SpiceCtrlMenu *menu) +{ + GHashTable *windows = virt_viewer_app_get_windows(VIRT_VIEWER_APP(self)); + RemoteViewerPrivate *priv = self->priv; + gboolean visible; + + DEBUG_LOG("Spice controller menu updated"); + + if (priv->controller_menu != NULL) { + g_object_unref (priv->controller_menu); + priv->controller_menu = NULL; + } + + if (menu && g_list_length(menu->items) > 0) { + priv->controller_menu = ctrlmenu_to_gtkmenu(self, menu); + g_hash_table_foreach(windows, spice_menu_update, self); + } + + visible = priv->controller_menu != NULL; + + remote_viewer_window_spice_menu_set_visible(self, visible); +} + +static SpiceSession * +remote_viewer_get_spice_session(RemoteViewer *self) +{ + VirtViewerSession *vsession = NULL; + SpiceSession *session = NULL; + + g_object_get(self, "session", &vsession, NULL); + g_return_val_if_fail(vsession != NULL, NULL); + + g_object_get(vsession, "spice-session", &session, NULL); + + g_object_unref(vsession); + + return session; +} + +#ifndef G_VALUE_INIT /* see bug https://bugzilla.gnome.org/show_bug.cgi?id=654793 */ +#define G_VALUE_INIT { 0, { { 0 } } } +#endif + +static void +spice_ctrl_notified(SpiceCtrlController *ctrl, + GParamSpec *pspec, + RemoteViewer *self) +{ + SpiceSession *session = remote_viewer_get_spice_session(self); + GValue value = G_VALUE_INIT; + VirtViewerApp *app = VIRT_VIEWER_APP(self); + + g_return_if_fail(session != NULL); + + g_value_init(&value, pspec->value_type); + g_object_get_property(G_OBJECT(ctrl), pspec->name, &value); + + if (g_str_equal(pspec->name, "host") || + g_str_equal(pspec->name, "port") || + g_str_equal(pspec->name, "password") || + g_str_equal(pspec->name, "ca-file")) { + g_object_set_property(G_OBJECT(session), pspec->name, &value); + } else if (g_str_equal(pspec->name, "sport")) { + g_object_set_property(G_OBJECT(session), "tls-port", &value); + } else if (g_str_equal(pspec->name, "tls-ciphers")) { + g_object_set_property(G_OBJECT(session), "ciphers", &value); + } else if (g_str_equal(pspec->name, "host-subject")) { + g_object_set_property(G_OBJECT(session), "cert-subject", &value); + } else if (g_str_equal(pspec->name, "title")) { + g_object_set_property(G_OBJECT(app), "title", &value); + } else if (g_str_equal(pspec->name, "display-flags")) { + guint flags = g_value_get_uint(&value); + gboolean fullscreen = flags & CONTROLLER_SET_FULL_SCREEN; + gboolean auto_res = flags & CONTROLLER_AUTO_DISPLAY_RES; + g_object_set(G_OBJECT(self), "fullscreen", fullscreen, NULL); + g_debug("unimplemented resize-guest %d", auto_res); + /* g_object_set(G_OBJECT(self), "resize-guest", auto_res, NULL); */ + } else if (g_str_equal(pspec->name, "menu")) { + spice_ctrl_menu_updated(self, g_value_get_object(&value)); + } else { + gchar *content = g_strdup_value_contents(&value); + + g_debug("unimplemented property: %s=%s", pspec->name, content); + g_free(content); + } + + g_object_unref(session); + g_value_unset(&value); +} + +static void +spice_ctrl_listen_async_cb(GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + GError *error = NULL; + + spice_ctrl_controller_listen_finish(SPICE_CTRL_CONTROLLER(object), res, &error); + + if (error != NULL) { + virt_viewer_app_simple_message_dialog(VIRT_VIEWER_APP(user_data), + _("Controller connection failed: %s"), + error->message); + g_clear_error(&error); + exit(1); /* TODO: make start async? */ + } +} + +static int +remote_viewer_activate(VirtViewerApp *app) +{ + g_return_val_if_fail(REMOTE_VIEWER_IS(app), -1); + RemoteViewer *self = REMOTE_VIEWER(app); + int ret = -1; + + if (self->priv->controller) { + SpiceSession *session = remote_viewer_get_spice_session(self); + ret = spice_session_connect(session); + g_object_unref(session); + } else { + ret = VIRT_VIEWER_APP_CLASS(remote_viewer_parent_class)->activate(app); + } + + return ret; +} + +static void +remote_viewer_window_added(VirtViewerApp *self G_GNUC_UNUSED, + VirtViewerWindow *win) +{ + GtkMenuShell *shell = GTK_MENU_SHELL(gtk_builder_get_object(virt_viewer_window_get_builder(win), "top-menu")); + GtkWidget *spice = gtk_menu_item_new_with_label("Spice"); + + gtk_menu_shell_append(shell, spice); + g_object_set_data(G_OBJECT(win), "spice-menu", spice); +} + static gboolean remote_viewer_start(VirtViewerApp *app) { - gchar *guri; - gchar *type; + g_return_val_if_fail(REMOTE_VIEWER_IS(app), FALSE); + + RemoteViewer *self = REMOTE_VIEWER(app); + RemoteViewerPrivate *priv = self->priv; gboolean ret = FALSE; + gchar *guri = NULL; + gchar *type = NULL; - g_object_get(app, "guri", &guri, NULL); - g_return_val_if_fail(guri != NULL, FALSE); + if (priv->controller) { + if (virt_viewer_app_create_session(app, "spice") < 0) { + virt_viewer_app_simple_message_dialog(app, _("Couldn't create a Spice session")); + goto cleanup; + } - DEBUG_LOG("Opening display to %s", guri); + g_signal_connect(priv->controller, "notify", G_CALLBACK(spice_ctrl_notified), self); + g_signal_connect(priv->controller, "do_connect", G_CALLBACK(spice_ctrl_do_connect), self); + g_signal_connect(priv->controller, "show", G_CALLBACK(spice_ctrl_show), self); + g_signal_connect(priv->controller, "hide", G_CALLBACK(spice_ctrl_hide), self); - if (virt_viewer_util_extract_host(guri, &type, NULL, NULL, NULL, NULL) < 0) { - virt_viewer_app_simple_message_dialog(app, _("Cannot determine the connection type from URI")); - goto cleanup; - } + spice_ctrl_controller_listen(priv->controller, NULL, spice_ctrl_listen_async_cb, self); + virt_viewer_app_show_status(VIRT_VIEWER_APP(self), _("Setting up Spice session...")); + } else { + g_object_get(app, "guri", &guri, NULL); + g_return_val_if_fail(guri != NULL, FALSE); - if (virt_viewer_app_create_session(app, type) < 0) { - virt_viewer_app_simple_message_dialog(app, _("Couldn't create a session for this type: %s"), type); - goto cleanup; - } + DEBUG_LOG("Opening display to %s", guri); + g_object_set(app, "title", guri, NULL); + + if (virt_viewer_util_extract_host(guri, &type, NULL, NULL, NULL, NULL) < 0) { + virt_viewer_app_simple_message_dialog(app, _("Cannot determine the connection type from URI")); + goto cleanup; + } + + if (virt_viewer_app_create_session(app, type) < 0) { + virt_viewer_app_simple_message_dialog(app, _("Couldn't create a session for this type: %s"), type); + goto cleanup; + } + + if (virt_viewer_app_initial_connect(app) < 0) { + virt_viewer_app_simple_message_dialog(app, _("Failed to initiate connection")); + goto cleanup; + } - if (virt_viewer_app_activate(app) < 0) { - virt_viewer_app_simple_message_dialog(app, _("Failed to initiate connection")); - goto cleanup; } ret = VIRT_VIEWER_APP_CLASS(remote_viewer_parent_class)->start(app); - cleanup: +cleanup: g_free(guri); g_free(type); return ret; diff --git a/src/remote-viewer.h b/src/remote-viewer.h index 1ff6d8d..3d02315 100644 --- a/src/remote-viewer.h +++ b/src/remote-viewer.h @@ -48,8 +48,8 @@ typedef struct { GType remote_viewer_get_type (void); -RemoteViewer * -remote_viewer_new(const gchar *uri, gboolean verbose); +RemoteViewer* remote_viewer_new(const gchar *uri, gboolean verbose); +RemoteViewer* remote_viewer_new_with_controller(gboolean verbose); G_END_DECLS diff --git a/src/virt-viewer-app.c b/src/virt-viewer-app.c index 97b53c2..352f206 100644 --- a/src/virt-viewer-app.c +++ b/src/virt-viewer-app.c @@ -1528,6 +1528,13 @@ virt_viewer_app_show_display(VirtViewerApp *self) g_hash_table_foreach(self->priv->windows, show_display_cb, self); } +GHashTable* +virt_viewer_app_get_windows(VirtViewerApp *self) +{ + g_return_val_if_fail(VIRT_VIEWER_IS_APP(self), NULL); + return self->priv->windows; +} + /* * Local variables: * c-indent-level: 8 diff --git a/src/virt-viewer-app.h b/src/virt-viewer-app.h index 7c3f0a7..320e75c 100644 --- a/src/virt-viewer-app.h +++ b/src/virt-viewer-app.h @@ -86,6 +86,7 @@ void virt_viewer_app_set_connect_info(VirtViewerApp *self, gboolean virt_viewer_app_window_set_visible(VirtViewerApp *self, VirtViewerWindow *window, gboolean visible); void virt_viewer_app_show_status(VirtViewerApp *self, const gchar *fmt, ...); void virt_viewer_app_show_display(VirtViewerApp *self); +GHashTable* virt_viewer_app_get_windows(VirtViewerApp *self); G_END_DECLS diff --git a/src/virt-viewer-session-spice.c b/src/virt-viewer-session-spice.c index 066f922..f89d042 100644 --- a/src/virt-viewer-session-spice.c +++ b/src/virt-viewer-session-spice.c @@ -41,6 +41,12 @@ struct _VirtViewerSessionSpicePrivate { #define VIRT_VIEWER_SESSION_SPICE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), VIRT_VIEWER_TYPE_SESSION_SPICE, VirtViewerSessionSpicePrivate)) +enum { + PROP_0, + PROP_SPICE_SESSION, +}; + + static void virt_viewer_session_spice_close(VirtViewerSession *session); static gboolean virt_viewer_session_spice_open_fd(VirtViewerSession *session, int fd); static gboolean virt_viewer_session_spice_open_host(VirtViewerSession *session, char *host, char *port); @@ -55,7 +61,33 @@ static void virt_viewer_session_spice_channel_destroy(SpiceSession *s, static void -virt_viewer_session_spice_finalize(GObject *obj) +virt_viewer_session_spice_get_property(GObject *object, guint property_id, + GValue *value, GParamSpec *pspec) +{ + VirtViewerSessionSpice *self = VIRT_VIEWER_SESSION_SPICE(object); + VirtViewerSessionSpicePrivate *priv = self->priv; + + switch (property_id) { + case PROP_SPICE_SESSION: + g_value_set_object(value, priv->session); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +virt_viewer_session_spice_set_property(GObject *object, guint property_id, + const GValue *value G_GNUC_UNUSED, GParamSpec *pspec) +{ + switch (property_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +virt_viewer_session_spice_dispose(GObject *obj) { VirtViewerSessionSpice *spice = VIRT_VIEWER_SESSION_SPICE(obj); @@ -76,7 +108,9 @@ virt_viewer_session_spice_class_init(VirtViewerSessionSpiceClass *klass) VirtViewerSessionClass *dclass = VIRT_VIEWER_SESSION_CLASS(klass); GObjectClass *oclass = G_OBJECT_CLASS(klass); - oclass->finalize = virt_viewer_session_spice_finalize; + oclass->get_property = virt_viewer_session_spice_get_property; + oclass->set_property = virt_viewer_session_spice_set_property; + oclass->dispose = virt_viewer_session_spice_dispose; dclass->close = virt_viewer_session_spice_close; dclass->open_fd = virt_viewer_session_spice_open_fd; @@ -85,6 +119,15 @@ virt_viewer_session_spice_class_init(VirtViewerSessionSpiceClass *klass) dclass->channel_open_fd = virt_viewer_session_spice_channel_open_fd; g_type_class_add_private(oclass, sizeof(VirtViewerSessionSpicePrivate)); + + g_object_class_install_property(oclass, + PROP_SPICE_SESSION, + g_param_spec_object("spice-session", + "Spice session", + "Spice session", + SPICE_TYPE_SESSION, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); } static void diff --git a/src/virt-viewer-window.c b/src/virt-viewer-window.c index 324e37f..d80b456 100644 --- a/src/virt-viewer-window.c +++ b/src/virt-viewer-window.c @@ -912,9 +912,18 @@ GtkMenuItem* virt_viewer_window_get_menu_displays(VirtViewerWindow *self) { g_return_val_if_fail(VIRT_VIEWER_IS_WINDOW(self), NULL); + return GTK_MENU_ITEM(gtk_builder_get_object(self->priv->builder, "menu-displays")); } +GtkBuilder* +virt_viewer_window_get_builder(VirtViewerWindow *self) +{ + g_return_val_if_fail(VIRT_VIEWER_IS_WINDOW(self), NULL); + + return self->priv->builder; +} + /* * Local variables: * c-indent-level: 8 diff --git a/src/virt-viewer-window.h b/src/virt-viewer-window.h index 9baab76..cf66f5e 100644 --- a/src/virt-viewer-window.h +++ b/src/virt-viewer-window.h @@ -69,6 +69,7 @@ gint virt_viewer_window_get_zoom_level(VirtViewerWindow *self); void virt_viewer_window_leave_fullscreen(VirtViewerWindow *self); void virt_viewer_window_enter_fullscreen(VirtViewerWindow *self, gboolean move, gint x, gint y); GtkMenuItem *virt_viewer_window_get_menu_displays(VirtViewerWindow *self); +GtkBuilder* virt_viewer_window_get_builder(VirtViewerWindow *window); G_END_DECLS -- 1.7.7.3