/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* 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_TERMIOS_H #include #endif #ifdef USE_SMARTCARD_012 #include #endif #include "spice-widget.h" #include "spice-gtk-session.h" #include "spice-audio.h" #include "spice-common.h" #include "spice-cmdline.h" #include "spice-option.h" #include "usb-device-widget.h" #include "spicy-connect.h" typedef struct spice_connection spice_connection; enum { STATE_SCROLL_LOCK, STATE_CAPS_LOCK, STATE_NUM_LOCK, STATE_MAX, }; #define SPICE_TYPE_WINDOW (spice_window_get_type ()) #define SPICE_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_WINDOW, SpiceWindow)) #define SPICE_IS_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_WINDOW)) #define SPICE_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_WINDOW, SpiceWindowClass)) #define SPICE_IS_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_WINDOW)) #define SPICE_WINDOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_WINDOW, SpiceWindowClass)) typedef struct _SpiceWindow SpiceWindow; typedef struct _SpiceWindowClass SpiceWindowClass; struct _SpiceWindow { GObject object; spice_connection *conn; gint id; gint monitor_id; GtkWidget *toplevel, *spice; GtkWidget *menubar, *toolbar; GtkWidget *ritem, *rmenu; GtkWidget *statusbar, *status, *st[STATE_MAX]; GtkActionGroup *ag; GtkUIManager *ui; bool fullscreen; bool mouse_grabbed; SpiceChannel *display_channel; #ifdef G_OS_WIN32 gint win_x; gint win_y; #endif bool enable_accels_save; bool enable_mnemonics_save; }; struct _SpiceWindowClass { GObjectClass parent_class; }; static GType spice_window_get_type(void); G_DEFINE_TYPE (SpiceWindow, spice_window, G_TYPE_OBJECT); #define CHANNELID_MAX 4 #define MONITORID_MAX 4 // FIXME: turn this into an object, get rid of fixed wins array, use // signals to replace the various callback that iterate over wins array struct spice_connection { SpiceSession *session; SpiceGtkSession *gtk_session; SpiceMainChannel *main; SpiceWindow *wins[CHANNELID_MAX * MONITORID_MAX]; SpiceAudio *audio; const char *mouse_state; const char *agent_state; gboolean agent_connected; int channels; int disconnecting; /* key: SpiceFileTransferTask, value: TransferTaskWidgets */ GHashTable *transfers; GtkWidget *transfer_dialog; }; static spice_connection *connection_new(void); static void connection_connect(spice_connection *conn); static void connection_disconnect(spice_connection *conn); static void connection_destroy(spice_connection *conn); static void usb_connect_failed(GObject *object, SpiceUsbDevice *device, GError *error, gpointer data); static gboolean is_gtk_session_property(const gchar *property); static void del_window(spice_connection *conn, SpiceWindow *win); /* options */ static gboolean fullscreen = false; static gboolean version = false; static char *spicy_title = NULL; /* globals */ static GMainLoop *mainloop = NULL; static int connections = 0; static GKeyFile *keyfile = NULL; static SpicePortChannel*stdin_port = NULL; /* ------------------------------------------------------------------ */ static int ask_user(GtkWidget *parent, char *title, char *message, char *dest, int dlen, int hide) { GtkWidget *dialog, *area, *label, *entry; const char *txt; int retval; /* Create the widgets */ dialog = gtk_dialog_new_with_buttons(title, parent ? GTK_WINDOW(parent) : NULL, GTK_DIALOG_DESTROY_WITH_PARENT, "_OK", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_REJECT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); label = gtk_label_new(message); gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); gtk_box_pack_start(GTK_BOX(area), label, FALSE, FALSE, 5); entry = gtk_entry_new(); gtk_entry_set_text(GTK_ENTRY(entry), dest); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); if (hide) gtk_entry_set_visibility(GTK_ENTRY(entry), FALSE); gtk_box_pack_start(GTK_BOX(area), entry, FALSE, FALSE, 5); /* show and wait for response */ gtk_widget_show_all(dialog); switch (gtk_dialog_run(GTK_DIALOG(dialog))) { case GTK_RESPONSE_ACCEPT: txt = gtk_entry_get_text(GTK_ENTRY(entry)); snprintf(dest, dlen, "%s", txt); retval = 0; break; default: retval = -1; break; } gtk_widget_destroy(dialog); return retval; } static void update_status_window(SpiceWindow *win) { gchar *status; if (win == NULL) return; if (win->mouse_grabbed) { SpiceGrabSequence *sequence = spice_display_get_grab_keys(SPICE_DISPLAY(win->spice)); gchar *seq = spice_grab_sequence_as_string(sequence); status = g_strdup_printf("Use %s to ungrab mouse.", seq); g_free(seq); } else { status = g_strdup_printf("mouse: %s, agent: %s", win->conn->mouse_state, win->conn->agent_state); } gtk_label_set_text(GTK_LABEL(win->status), status); g_free(status); } static void update_status(struct spice_connection *conn) { int i; for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { if (conn->wins[i] == NULL) continue; update_status_window(conn->wins[i]); } } static const char *spice_edit_properties[] = { "CopyToGuest", "PasteFromGuest", }; static void update_edit_menu_window(SpiceWindow *win) { int i; GtkAction *toggle; if (win == NULL) { return; } /* Make "CopyToGuest" and "PasteFromGuest" insensitive if spice * agent is not connected */ for (i = 0; i < G_N_ELEMENTS(spice_edit_properties); i++) { toggle = gtk_action_group_get_action(win->ag, spice_edit_properties[i]); if (toggle) { gtk_action_set_sensitive(toggle, win->conn->agent_connected); } } } static void update_edit_menu(struct spice_connection *conn) { int i; for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { if (conn->wins[i]) { update_edit_menu_window(conn->wins[i]); } } } static void menu_cb_connect(GtkAction *action, void *data) { struct spice_connection *conn; conn = connection_new(); connection_connect(conn); } static void menu_cb_close(GtkAction *action, void *data) { SpiceWindow *win = data; connection_disconnect(win->conn); } static void menu_cb_copy(GtkAction *action, void *data) { SpiceWindow *win = data; spice_gtk_session_copy_to_guest(win->conn->gtk_session); } static void menu_cb_paste(GtkAction *action, void *data) { SpiceWindow *win = data; spice_gtk_session_paste_from_guest(win->conn->gtk_session); } static void window_set_fullscreen(SpiceWindow *win, gboolean fs) { if (fs) { #ifdef G_OS_WIN32 gtk_window_get_position(GTK_WINDOW(win->toplevel), &win->win_x, &win->win_y); #endif gtk_window_fullscreen(GTK_WINDOW(win->toplevel)); } else { gtk_window_unfullscreen(GTK_WINDOW(win->toplevel)); #ifdef G_OS_WIN32 gtk_window_move(GTK_WINDOW(win->toplevel), win->win_x, win->win_y); #endif } } static void menu_cb_fullscreen(GtkAction *action, void *data) { SpiceWindow *win = data; window_set_fullscreen(win, !win->fullscreen); } #ifdef USE_SMARTCARD static void enable_smartcard_actions(SpiceWindow *win, VReader *reader, gboolean can_insert, gboolean can_remove) { GtkAction *action; if ((reader != NULL) && (!spice_smartcard_reader_is_software((SpiceSmartcardReader*)reader))) { /* Having menu actions to insert/remove smartcards only makes sense * for software smartcard readers, don't do anything when the event * we received was for a "real" smartcard reader. */ return; } action = gtk_action_group_get_action(win->ag, "InsertSmartcard"); g_return_if_fail(action != NULL); gtk_action_set_sensitive(action, can_insert); action = gtk_action_group_get_action(win->ag, "RemoveSmartcard"); g_return_if_fail(action != NULL); gtk_action_set_sensitive(action, can_remove); } static void reader_added_cb(SpiceSmartcardManager *manager, VReader *reader, gpointer user_data) { enable_smartcard_actions(user_data, reader, TRUE, FALSE); } static void reader_removed_cb(SpiceSmartcardManager *manager, VReader *reader, gpointer user_data) { enable_smartcard_actions(user_data, reader, FALSE, FALSE); } static void card_inserted_cb(SpiceSmartcardManager *manager, VReader *reader, gpointer user_data) { enable_smartcard_actions(user_data, reader, FALSE, TRUE); } static void card_removed_cb(SpiceSmartcardManager *manager, VReader *reader, gpointer user_data) { enable_smartcard_actions(user_data, reader, TRUE, FALSE); } static void menu_cb_insert_smartcard(GtkAction *action, void *data) { spice_smartcard_manager_insert_card(spice_smartcard_manager_get()); } static void menu_cb_remove_smartcard(GtkAction *action, void *data) { spice_smartcard_manager_remove_card(spice_smartcard_manager_get()); } #endif static void menu_cb_mouse_mode(GtkAction *action, void *data) { SpiceWindow *win = data; SpiceMainChannel *cmain = win->conn->main; int mode; g_object_get(cmain, "mouse-mode", &mode, NULL); if (mode == SPICE_MOUSE_MODE_CLIENT) mode = SPICE_MOUSE_MODE_SERVER; else mode = SPICE_MOUSE_MODE_CLIENT; spice_main_request_mouse_mode(cmain, mode); } #ifdef USE_USBREDIR static void remove_cb(GtkContainer *container, GtkWidget *widget, void *data) { gtk_window_resize(GTK_WINDOW(data), 1, 1); } static void menu_cb_select_usb_devices(GtkAction *action, void *data) { GtkWidget *dialog, *area, *usb_device_widget; SpiceWindow *win = data; /* Create the widgets */ dialog = gtk_dialog_new_with_buttons( "Select USB devices for redirection", GTK_WINDOW(win->toplevel), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "_Close", GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); gtk_container_set_border_width(GTK_CONTAINER(dialog), 12); gtk_box_set_spacing(GTK_BOX(gtk_bin_get_child(GTK_BIN(dialog))), 12); area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); usb_device_widget = spice_usb_device_widget_new(win->conn->session, NULL); /* default format */ g_signal_connect(usb_device_widget, "connect-failed", G_CALLBACK(usb_connect_failed), NULL); gtk_box_pack_start(GTK_BOX(area), usb_device_widget, TRUE, TRUE, 0); /* This shrinks the dialog when USB devices are unplugged */ g_signal_connect(usb_device_widget, "remove", G_CALLBACK(remove_cb), dialog); /* show and run */ gtk_widget_show_all(dialog); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); } #endif static void menu_cb_bool_prop(GtkToggleAction *action, gpointer data) { SpiceWindow *win = data; gboolean state = gtk_toggle_action_get_active(action); const char *name; gpointer object; name = gtk_action_get_name(GTK_ACTION(action)); SPICE_DEBUG("%s: %s = %s", __FUNCTION__, name, state ? "yes" : "no"); g_key_file_set_boolean(keyfile, "general", name, state); if (is_gtk_session_property(name)) { object = win->conn->gtk_session; } else { object = win->spice; } g_object_set(object, name, state, NULL); } static void menu_cb_conn_bool_prop_changed(GObject *gobject, GParamSpec *pspec, gpointer user_data) { SpiceWindow *win = user_data; const gchar *property = g_param_spec_get_name(pspec); GtkAction *toggle; gboolean state; toggle = gtk_action_group_get_action(win->ag, property); g_object_get(win->conn->gtk_session, property, &state, NULL); gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); } static void menu_cb_toolbar(GtkToggleAction *action, gpointer data) { SpiceWindow *win = data; gboolean state = gtk_toggle_action_get_active(action); gtk_widget_set_visible(win->toolbar, state); g_key_file_set_boolean(keyfile, "ui", "toolbar", state); } static void menu_cb_statusbar(GtkToggleAction *action, gpointer data) { SpiceWindow *win = data; gboolean state = gtk_toggle_action_get_active(action); gtk_widget_set_visible(win->statusbar, state); g_key_file_set_boolean(keyfile, "ui", "statusbar", state); } static void menu_cb_about(GtkAction *action, void *data) { char *comments = "gtk test client app for the\n" "spice remote desktop protocol"; static const char *copyright = "(c) 2010 Red Hat"; static const char *website = "http://www.spice-space.org"; static const char *authors[] = { "Gerd Hoffmann ", "Marc-André Lureau ", NULL }; SpiceWindow *win = data; gtk_show_about_dialog(GTK_WINDOW(win->toplevel), "authors", authors, "comments", comments, "copyright", copyright, "logo-icon-name", "help-about", "website", website, "version", PACKAGE_VERSION, "license", "LGPLv2.1", NULL); } static gboolean delete_cb(GtkWidget *widget, GdkEvent *event, gpointer data) { SpiceWindow *win = data; if (win->monitor_id == 0) connection_disconnect(win->conn); else del_window(win->conn, win); return true; } static gboolean window_state_cb(GtkWidget *widget, GdkEventWindowState *event, gpointer data) { SpiceWindow *win = data; if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) { win->fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; if (win->fullscreen) { gtk_widget_hide(win->menubar); gtk_widget_hide(win->toolbar); gtk_widget_hide(win->statusbar); gtk_widget_grab_focus(win->spice); } else { gboolean state; GtkAction *toggle; gtk_widget_show(win->menubar); toggle = gtk_action_group_get_action(win->ag, "Toolbar"); state = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(toggle)); gtk_widget_set_visible(win->toolbar, state); toggle = gtk_action_group_get_action(win->ag, "Statusbar"); state = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(toggle)); gtk_widget_set_visible(win->statusbar, state); } } return TRUE; } static void grab_keys_pressed_cb(GtkWidget *widget, gpointer data) { SpiceWindow *win = data; /* since mnemonics are disabled, we leave fullscreen when ungrabbing mouse. Perhaps we should have a different handling of fullscreen key, or simply use a UI, like vinagre */ window_set_fullscreen(win, FALSE); } static void mouse_grab_cb(GtkWidget *widget, gint grabbed, gpointer data) { SpiceWindow *win = data; win->mouse_grabbed = grabbed; update_status(win->conn); } static void keyboard_grab_cb(GtkWidget *widget, gint grabbed, gpointer data) { SpiceWindow *win = data; GtkSettings *settings = gtk_widget_get_settings (widget); if (grabbed) { /* disable mnemonics & accels */ g_object_get(settings, "gtk-enable-accels", &win->enable_accels_save, "gtk-enable-mnemonics", &win->enable_mnemonics_save, NULL); g_object_set(settings, "gtk-enable-accels", FALSE, "gtk-enable-mnemonics", FALSE, NULL); } else { g_object_set(settings, "gtk-enable-accels", win->enable_accels_save, "gtk-enable-mnemonics", win->enable_mnemonics_save, NULL); } } static void restore_configuration(SpiceWindow *win) { gboolean state; gchar *str; gchar **keys = NULL; gsize nkeys, i; GError *error = NULL; gpointer object; keys = g_key_file_get_keys(keyfile, "general", &nkeys, &error); if (error != NULL) { if (error->code != G_KEY_FILE_ERROR_GROUP_NOT_FOUND) g_warning("Failed to read configuration file keys: %s", error->message); g_clear_error(&error); return; } if (nkeys > 0) g_return_if_fail(keys != NULL); for (i = 0; i < nkeys; ++i) { if (g_str_equal(keys[i], "grab-sequence")) continue; state = g_key_file_get_boolean(keyfile, "general", keys[i], &error); if (error != NULL) { g_clear_error(&error); continue; } if (is_gtk_session_property(keys[i])) { object = win->conn->gtk_session; } else { object = win->spice; } g_object_set(object, keys[i], state, NULL); } g_strfreev(keys); str = g_key_file_get_string(keyfile, "general", "grab-sequence", &error); if (error == NULL) { SpiceGrabSequence *seq = spice_grab_sequence_new_from_string(str); spice_display_set_grab_keys(SPICE_DISPLAY(win->spice), seq); spice_grab_sequence_free(seq); g_free(str); } g_clear_error(&error); state = g_key_file_get_boolean(keyfile, "ui", "toolbar", &error); if (error == NULL) gtk_widget_set_visible(win->toolbar, state); g_clear_error(&error); state = g_key_file_get_boolean(keyfile, "ui", "statusbar", &error); if (error == NULL) gtk_widget_set_visible(win->statusbar, state); g_clear_error(&error); } /* ------------------------------------------------------------------ */ static const GtkActionEntry entries[] = { { .name = "FileMenu", .label = "_File", },{ .name = "FileRecentMenu", .label = "_Recent", },{ .name = "EditMenu", .label = "_Edit", },{ .name = "ViewMenu", .label = "_View", },{ .name = "InputMenu", .label = "_Input", },{ .name = "OptionMenu", .label = "_Options", },{ .name = "CompressionMenu", .label = "_Preferred image compression", },{ .name = "VideoCodecTypeMenu", .label = "_Preferred video codec type", },{ .name = "HelpMenu", .label = "_Help", },{ /* File menu */ .name = "Connect", .stock_id = "_Connect", .label = "_Connect ...", .callback = G_CALLBACK(menu_cb_connect), },{ .name = "Close", .stock_id = "window-close", .label = "_Close", .callback = G_CALLBACK(menu_cb_close), .accelerator = "", /* none (disable default "W") */ },{ /* Edit menu */ .name = "CopyToGuest", .stock_id = "edit-copy", .label = "_Copy to guest", .callback = G_CALLBACK(menu_cb_copy), .accelerator = "", /* none (disable default "C") */ },{ .name = "PasteFromGuest", .stock_id = "edit-paste", .label = "_Paste from guest", .callback = G_CALLBACK(menu_cb_paste), .accelerator = "", /* none (disable default "V") */ },{ /* View menu */ .name = "Fullscreen", .stock_id = "view-fullscreen", .label = "_Fullscreen", .callback = G_CALLBACK(menu_cb_fullscreen), .accelerator = "F11", },{ #ifdef USE_SMARTCARD .name = "InsertSmartcard", .label = "_Insert Smartcard", .callback = G_CALLBACK(menu_cb_insert_smartcard), .accelerator = "F8", },{ .name = "RemoveSmartcard", .label = "_Remove Smartcard", .callback = G_CALLBACK(menu_cb_remove_smartcard), .accelerator = "F9", },{ #endif #ifdef USE_USBREDIR .name = "SelectUsbDevices", .label = "_Select USB Devices for redirection", .callback = G_CALLBACK(menu_cb_select_usb_devices), .accelerator = "F10", },{ #endif .name = "MouseMode", .label = "Toggle _mouse mode", .callback = G_CALLBACK(menu_cb_mouse_mode), .accelerator = "F7", },{ /* Help menu */ .name = "About", .stock_id = "help-about", .label = "_About ...", .callback = G_CALLBACK(menu_cb_about), } }; static const char *spice_display_properties[] = { "grab-keyboard", "grab-mouse", "resize-guest", "scaling", "disable-inputs", }; static const char *spice_gtk_session_properties[] = { "auto-clipboard", "auto-usbredir", "sync-modifiers", }; static const GtkToggleActionEntry tentries[] = { { .name = "grab-keyboard", .label = "Grab keyboard when active and focused", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "grab-mouse", .label = "Grab mouse in server mode (no tablet/vdagent)", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "resize-guest", .label = "Resize guest to match window size", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "scaling", .label = "Scale display", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "disable-inputs", .label = "Disable inputs", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "sync-modifiers", .label = "Sync modifiers", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "auto-clipboard", .label = "Automatic clipboard sharing between host and guest", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "auto-usbredir", .label = "Auto redirect newly plugged in USB devices", .callback = G_CALLBACK(menu_cb_bool_prop), },{ .name = "Statusbar", .label = "Statusbar", .callback = G_CALLBACK(menu_cb_statusbar), },{ .name = "Toolbar", .label = "Toolbar", .callback = G_CALLBACK(menu_cb_toolbar), } }; static const GtkRadioActionEntry compression_entries[] = { { .name = "auto-glz", .label = "auto-glz", .value = SPICE_IMAGE_COMPRESSION_AUTO_GLZ, },{ .name = "auto-lz", .label = "auto-lz", .value = SPICE_IMAGE_COMPRESSION_AUTO_LZ, },{ .name = "quic", .label = "quic", .value = SPICE_IMAGE_COMPRESSION_QUIC, },{ .name = "glz", .label = "glz", .value = SPICE_IMAGE_COMPRESSION_GLZ, },{ .name = "lz", .label = "lz", .value = SPICE_IMAGE_COMPRESSION_LZ, },{ #ifdef USE_LZ4 .name = "lz4", .label = "lz4", .value = SPICE_IMAGE_COMPRESSION_LZ4, },{ #endif .name = "off", .label = "off", .value = SPICE_IMAGE_COMPRESSION_OFF, } }; static const GtkRadioActionEntry video_codec_type_entries[] = { { .name = "mjpeg", .label = "mjpeg", .value = SPICE_VIDEO_CODEC_TYPE_MJPEG, },{ .name = "vp8", .label = "vp8", .value = SPICE_VIDEO_CODEC_TYPE_VP8, },{ .name = "vp9", .label = "vp9", .value = SPICE_VIDEO_CODEC_TYPE_VP9, },{ .name = "h264", .label = "h264", .value = SPICE_VIDEO_CODEC_TYPE_H264, } }; static char ui_xml[] = "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" #ifdef USE_SMARTCARD " \n" " \n" #endif #ifdef USE_USBREDIR " \n" #endif " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" #ifdef USE_LZ4 " \n" #endif " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; static gboolean is_gtk_session_property(const gchar *property) { int i; for (i = 0; i < G_N_ELEMENTS(spice_gtk_session_properties); i++) { if (!strcmp(spice_gtk_session_properties[i], property)) { return TRUE; } } return FALSE; } static void recent_item_activated_cb(GtkRecentChooser *chooser, gpointer data) { GtkRecentInfo *info; struct spice_connection *conn; const char *uri; info = gtk_recent_chooser_get_current_item(chooser); uri = gtk_recent_info_get_uri(info); g_return_if_fail(uri != NULL); conn = connection_new(); g_object_set(conn->session, "uri", uri, NULL); gtk_recent_info_unref(info); connection_connect(conn); } static void compression_cb(GtkRadioAction *action G_GNUC_UNUSED, GtkRadioAction *current, gpointer user_data) { spice_display_change_preferred_compression(SPICE_CHANNEL(user_data), gtk_radio_action_get_current_value(current)); } static void video_codec_type_cb(GtkRadioAction *action G_GNUC_UNUSED, GtkRadioAction *current, gpointer user_data) { spice_display_change_preferred_video_codec_type(SPICE_CHANNEL(user_data), gtk_radio_action_get_current_value(current)); } static void spice_window_class_init (SpiceWindowClass *klass) { } static void spice_window_init (SpiceWindow *self) { } static SpiceWindow *create_spice_window(spice_connection *conn, SpiceChannel *channel, int id, gint monitor_id) { char title[32]; SpiceWindow *win; GtkAction *toggle; gboolean state; GtkWidget *vbox, *frame; GError *err = NULL; int i; SpiceGrabSequence *seq; win = g_object_new(SPICE_TYPE_WINDOW, NULL); win->id = id; win->monitor_id = monitor_id; win->conn = conn; win->display_channel = channel; /* toplevel */ win->toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL); if (spicy_title == NULL) { snprintf(title, sizeof(title), "spice display %d:%d", id, monitor_id); } else { snprintf(title, sizeof(title), "%s", spicy_title); } 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); g_signal_connect(G_OBJECT(win->toplevel), "delete-event", G_CALLBACK(delete_cb), win); /* menu + toolbar */ win->ui = gtk_ui_manager_new(); win->ag = gtk_action_group_new("MenuActions"); gtk_action_group_add_actions(win->ag, entries, G_N_ELEMENTS(entries), win); gtk_action_group_add_toggle_actions(win->ag, tentries, G_N_ELEMENTS(tentries), win); gtk_action_group_add_radio_actions(win->ag, compression_entries, G_N_ELEMENTS(compression_entries), -1, G_CALLBACK(compression_cb), win->display_channel); if (!spice_channel_test_capability(win->display_channel, SPICE_DISPLAY_CAP_PREF_COMPRESSION)) { GtkAction *compression_menu_action = gtk_action_group_get_action(win->ag, "CompressionMenu"); gtk_action_set_sensitive(compression_menu_action, FALSE); } gtk_action_group_add_radio_actions(win->ag, video_codec_type_entries, G_N_ELEMENTS(video_codec_type_entries), -1, G_CALLBACK(video_codec_type_cb), win->display_channel); if (!spice_channel_test_capability(win->display_channel, SPICE_DISPLAY_CAP_PREF_VIDEO_CODEC_TYPE)) { GtkAction *video_codec_type_menu_action = gtk_action_group_get_action(win->ag, "VideoCodecTypeMenu"); gtk_action_set_sensitive(video_codec_type_menu_action, FALSE); } gtk_ui_manager_insert_action_group(win->ui, win->ag, 0); gtk_window_add_accel_group(GTK_WINDOW(win->toplevel), gtk_ui_manager_get_accel_group(win->ui)); err = NULL; if (!gtk_ui_manager_add_ui_from_string(win->ui, ui_xml, -1, &err)) { g_warning("building menus failed: %s", err->message); g_error_free(err); exit(1); } win->menubar = gtk_ui_manager_get_widget(win->ui, "/MainMenu"); win->toolbar = gtk_ui_manager_get_widget(win->ui, "/ToolBar"); /* recent menu */ win->ritem = gtk_ui_manager_get_widget (win->ui, "/MainMenu/FileMenu/FileRecentMenu"); GtkRecentFilter *rfilter; win->rmenu = gtk_recent_chooser_menu_new(); gtk_recent_chooser_set_show_icons(GTK_RECENT_CHOOSER(win->rmenu), FALSE); rfilter = gtk_recent_filter_new(); gtk_recent_filter_add_mime_type(rfilter, "application/x-spice"); gtk_recent_chooser_add_filter(GTK_RECENT_CHOOSER(win->rmenu), rfilter); gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(win->rmenu), FALSE); gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->ritem), win->rmenu); g_signal_connect(win->rmenu, "item-activated", G_CALLBACK(recent_item_activated_cb), win); /* spice display */ win->spice = GTK_WIDGET(spice_display_new_with_monitor(conn->session, id, monitor_id)); seq = spice_grab_sequence_new_from_string("Shift_L+F12"); spice_display_set_grab_keys(SPICE_DISPLAY(win->spice), seq); spice_grab_sequence_free(seq); g_signal_connect(G_OBJECT(win->spice), "mouse-grab", G_CALLBACK(mouse_grab_cb), win); g_signal_connect(G_OBJECT(win->spice), "keyboard-grab", G_CALLBACK(keyboard_grab_cb), win); g_signal_connect(G_OBJECT(win->spice), "grab-keys-pressed", G_CALLBACK(grab_keys_pressed_cb), win); /* status line */ win->statusbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); win->status = gtk_label_new("status line"); gtk_misc_set_alignment(GTK_MISC(win->status), 0, 0.5); gtk_misc_set_padding(GTK_MISC(win->status), 3, 1); update_status_window(win); frame = gtk_frame_new(NULL); gtk_box_pack_start(GTK_BOX(win->statusbar), frame, TRUE, TRUE, 0); gtk_container_add(GTK_CONTAINER(frame), win->status); for (i = 0; i < STATE_MAX; i++) { win->st[i] = gtk_label_new("?"); gtk_label_set_width_chars(GTK_LABEL(win->st[i]), 5); frame = gtk_frame_new(NULL); gtk_box_pack_end(GTK_BOX(win->statusbar), frame, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(frame), win->st[i]); } /* Make a vbox and put stuff in */ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1); gtk_container_set_border_width(GTK_CONTAINER(vbox), 0); gtk_container_add(GTK_CONTAINER(win->toplevel), vbox); gtk_box_pack_start(GTK_BOX(vbox), win->menubar, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), win->toolbar, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), win->spice, TRUE, TRUE, 0); gtk_box_pack_end(GTK_BOX(vbox), win->statusbar, FALSE, TRUE, 0); /* show window */ if (fullscreen) gtk_window_fullscreen(GTK_WINDOW(win->toplevel)); gtk_widget_show_all(vbox); restore_configuration(win); /* init toggle actions */ for (i = 0; i < G_N_ELEMENTS(spice_display_properties); i++) { toggle = gtk_action_group_get_action(win->ag, spice_display_properties[i]); g_object_get(win->spice, spice_display_properties[i], &state, NULL); gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); } for (i = 0; i < G_N_ELEMENTS(spice_gtk_session_properties); i++) { char notify[64]; toggle = gtk_action_group_get_action(win->ag, spice_gtk_session_properties[i]); g_object_get(win->conn->gtk_session, spice_gtk_session_properties[i], &state, NULL); gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); snprintf(notify, sizeof(notify), "notify::%s", spice_gtk_session_properties[i]); spice_g_signal_connect_object(win->conn->gtk_session, notify, G_CALLBACK(menu_cb_conn_bool_prop_changed), win, 0); } update_edit_menu_window(win); toggle = gtk_action_group_get_action(win->ag, "Toolbar"); state = gtk_widget_get_visible(win->toolbar); gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); toggle = gtk_action_group_get_action(win->ag, "Statusbar"); state = gtk_widget_get_visible(win->statusbar); gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); #ifdef USE_SMARTCARD gboolean smartcard; enable_smartcard_actions(win, NULL, FALSE, FALSE); g_object_get(G_OBJECT(conn->session), "enable-smartcard", &smartcard, NULL); if (smartcard) { g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "reader-added", (GCallback)reader_added_cb, win); g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "reader-removed", (GCallback)reader_removed_cb, win); g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "card-inserted", (GCallback)card_inserted_cb, win); g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "card-removed", (GCallback)card_removed_cb, win); } #endif #ifndef USE_USBREDIR GtkAction *usbredir = gtk_action_group_get_action(win->ag, "auto-usbredir"); gtk_action_set_visible(usbredir, FALSE); #endif gtk_widget_grab_focus(win->spice); return win; } static void destroy_spice_window(SpiceWindow *win) { if (win == NULL) return; SPICE_DEBUG("destroy window (#%d:%d)", win->id, win->monitor_id); g_object_unref(win->ag); g_object_unref(win->ui); gtk_widget_destroy(win->toplevel); g_object_unref(win); } /* ------------------------------------------------------------------ */ static void recent_add(SpiceSession *session) { GtkRecentManager *recent; GtkRecentData meta = { .mime_type = (char*)"application/x-spice", .app_name = (char*)"spicy", .app_exec = (char*)"spicy --uri=%u", }; char *uri; g_object_get(session, "uri", &uri, NULL); SPICE_DEBUG("%s: %s", __FUNCTION__, uri); recent = gtk_recent_manager_get_default(); if (g_str_has_prefix(uri, "spice://")) meta.display_name = uri + 8; else if (g_str_has_prefix(uri, "spice+unix://")) meta.display_name = uri + 13; else g_return_if_reached(); if (!gtk_recent_manager_add_full(recent, uri, &meta)) g_warning("Recent item couldn't be added successfully"); g_free(uri); } static void main_channel_event(SpiceChannel *channel, SpiceChannelEvent event, gpointer data) { const GError *error = NULL; spice_connection *conn = data; char password[64]; int rc; switch (event) { case SPICE_CHANNEL_OPENED: g_message("main channel: opened"); recent_add(conn->session); break; case SPICE_CHANNEL_SWITCHING: g_message("main channel: switching host"); break; case SPICE_CHANNEL_CLOSED: /* this event is only sent if the channel was succesfully opened before */ g_message("main channel: closed"); connection_disconnect(conn); break; case SPICE_CHANNEL_ERROR_IO: connection_disconnect(conn); break; case SPICE_CHANNEL_ERROR_TLS: case SPICE_CHANNEL_ERROR_LINK: case SPICE_CHANNEL_ERROR_CONNECT: error = spice_channel_get_error(channel); g_message("main channel: failed to connect"); if (error) { g_message("channel error: %s", error->message); } if (spicy_connect_dialog(conn->session)) { connection_connect(conn); } else { connection_disconnect(conn); } break; case SPICE_CHANNEL_ERROR_AUTH: g_warning("main channel: auth failure (wrong password?)"); strcpy(password, ""); /* FIXME i18 */ rc = ask_user(NULL, "Authentication", "Please enter the spice server password", password, sizeof(password), true); if (rc == 0) { g_object_set(conn->session, "password", password, NULL); connection_connect(conn); } else { connection_disconnect(conn); } break; default: /* TODO: more sophisticated error handling */ g_warning("unknown main channel event: %u", event); /* connection_disconnect(conn); */ break; } } static void main_mouse_update(SpiceChannel *channel, gpointer data) { spice_connection *conn = data; gint mode; g_object_get(channel, "mouse-mode", &mode, NULL); switch (mode) { case SPICE_MOUSE_MODE_SERVER: conn->mouse_state = "server"; break; case SPICE_MOUSE_MODE_CLIENT: conn->mouse_state = "client"; break; default: conn->mouse_state = "?"; break; } update_status(conn); } static void main_agent_update(SpiceChannel *channel, gpointer data) { spice_connection *conn = data; g_object_get(channel, "agent-connected", &conn->agent_connected, NULL); conn->agent_state = conn->agent_connected ? "yes" : "no"; update_status(conn); update_edit_menu(conn); } static void inputs_modifiers(SpiceChannel *channel, gpointer data) { spice_connection *conn = data; int m, i; g_object_get(channel, "key-modifiers", &m, NULL); for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { if (conn->wins[i] == NULL) continue; gtk_label_set_text(GTK_LABEL(conn->wins[i]->st[STATE_SCROLL_LOCK]), m & SPICE_KEYBOARD_MODIFIER_FLAGS_SCROLL_LOCK ? "SCROLL" : ""); gtk_label_set_text(GTK_LABEL(conn->wins[i]->st[STATE_CAPS_LOCK]), m & SPICE_KEYBOARD_MODIFIER_FLAGS_CAPS_LOCK ? "CAPS" : ""); gtk_label_set_text(GTK_LABEL(conn->wins[i]->st[STATE_NUM_LOCK]), m & SPICE_KEYBOARD_MODIFIER_FLAGS_NUM_LOCK ? "NUM" : ""); } } static void display_mark(SpiceChannel *channel, gint mark, SpiceWindow *win) { g_return_if_fail(win != NULL); g_return_if_fail(win->toplevel != NULL); if (mark == TRUE) { gtk_widget_show(win->toplevel); } else { gtk_widget_hide(win->toplevel); } } static void update_auto_usbredir_sensitive(spice_connection *conn) { #ifdef USE_USBREDIR int i; GtkAction *ac; gboolean sensitive; sensitive = spice_session_has_channel_type(conn->session, SPICE_CHANNEL_USBREDIR); for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { if (conn->wins[i] == NULL) continue; ac = gtk_action_group_get_action(conn->wins[i]->ag, "auto-usbredir"); gtk_action_set_sensitive(ac, sensitive); } #endif } static SpiceWindow* get_window(spice_connection *conn, int channel_id, int monitor_id) { g_return_val_if_fail(channel_id < CHANNELID_MAX, NULL); g_return_val_if_fail(monitor_id < MONITORID_MAX, NULL); return conn->wins[channel_id * CHANNELID_MAX + monitor_id]; } static void add_window(spice_connection *conn, SpiceWindow *win) { g_return_if_fail(win != NULL); g_return_if_fail(win->id < CHANNELID_MAX); g_return_if_fail(win->monitor_id < MONITORID_MAX); g_return_if_fail(conn->wins[win->id * CHANNELID_MAX + win->monitor_id] == NULL); SPICE_DEBUG("add display monitor %d:%d", win->id, win->monitor_id); conn->wins[win->id * CHANNELID_MAX + win->monitor_id] = win; } static void del_window(spice_connection *conn, SpiceWindow *win) { if (win == NULL) return; g_return_if_fail(win->id < CHANNELID_MAX); g_return_if_fail(win->monitor_id < MONITORID_MAX); g_debug("del display monitor %d:%d", win->id, win->monitor_id); conn->wins[win->id * CHANNELID_MAX + win->monitor_id] = NULL; if (win->id > 0) spice_main_set_display_enabled(conn->main, win->id, FALSE); else spice_main_set_display_enabled(conn->main, win->monitor_id, FALSE); spice_main_send_monitor_config(conn->main); destroy_spice_window(win); } static void display_monitors(SpiceChannel *display, GParamSpec *pspec, spice_connection *conn) { GArray *monitors = NULL; int id; guint i; g_object_get(display, "channel-id", &id, "monitors", &monitors, NULL); g_return_if_fail(monitors != NULL); for (i = 0; i < monitors->len; i++) { SpiceWindow *w; if (!get_window(conn, id, i)) { w = create_spice_window(conn, display, id, i); add_window(conn, w); spice_g_signal_connect_object(display, "display-mark", G_CALLBACK(display_mark), w, 0); gtk_widget_show(w->toplevel); update_auto_usbredir_sensitive(conn); } } for (; i < MONITORID_MAX; i++) del_window(conn, get_window(conn, id, i)); g_clear_pointer(&monitors, g_array_unref); } static void port_write_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) { SpicePortChannel *port = SPICE_PORT_CHANNEL(source_object); GError *error = NULL; spice_port_write_finish(port, res, &error); if (error != NULL) g_warning("%s", error->message); g_clear_error(&error); } static void port_flushed_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) { SpiceChannel *channel = SPICE_CHANNEL(source_object); GError *error = NULL; spice_channel_flush_finish(channel, res, &error); if (error != NULL) g_warning("%s", error->message); g_clear_error(&error); spice_channel_disconnect(channel, SPICE_CHANNEL_CLOSED); } static gboolean input_cb(GIOChannel *gin, GIOCondition condition, gpointer data) { char buf[4096]; gsize bytes_read; GIOStatus status; if (!(condition & G_IO_IN)) return FALSE; status = g_io_channel_read_chars(gin, buf, sizeof(buf), &bytes_read, NULL); if (status != G_IO_STATUS_NORMAL) return FALSE; if (stdin_port != NULL) spice_port_write_async(stdin_port, buf, bytes_read, NULL, port_write_cb, NULL); return TRUE; } static void watch_stdin(void); static void port_opened(SpiceChannel *channel, GParamSpec *pspec, spice_connection *conn) { SpicePortChannel *port = SPICE_PORT_CHANNEL(channel); gchar *name = NULL; gboolean opened = FALSE; g_object_get(channel, "port-name", &name, "port-opened", &opened, NULL); g_printerr("port %p %s: %s\n", channel, name, opened ? "opened" : "closed"); if (opened) { /* only send a break event and disconnect */ if (g_strcmp0(name, "org.spice.spicy.break") == 0) { spice_port_event(port, SPICE_PORT_EVENT_BREAK); spice_channel_flush_async(channel, NULL, port_flushed_cb, conn); } /* handle the first spicy port and connect it to stdin/out */ if (g_strcmp0(name, "org.spice.spicy") == 0 && stdin_port == NULL) { watch_stdin(); stdin_port = port; } } else { if (port == stdin_port) stdin_port = NULL; } g_free(name); } static void port_data(SpicePortChannel *port, gpointer data, int size, spice_connection *conn) { int r; if (port != stdin_port) return; r = write(fileno(stdout), data, size); if (r != size) { g_warning("port write failed result %d/%d errno %d", r, size, errno); } } typedef struct { GtkWidget *vbox; GtkWidget *hbox; GtkWidget *progress; GtkWidget *label; GtkWidget *cancel; } TransferTaskWidgets; static void transfer_update_progress(GObject *object, GParamSpec *pspec, gpointer user_data) { spice_connection *conn = user_data; TransferTaskWidgets *widgets = g_hash_table_lookup(conn->transfers, object); g_return_if_fail(widgets); gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(widgets->progress), spice_file_transfer_task_get_progress(SPICE_FILE_TRANSFER_TASK(object))); } static void transfer_task_finished(SpiceFileTransferTask *task, GError *error, spice_connection *conn) { if (error) g_warning("%s", error->message); g_hash_table_remove(conn->transfers, task); if (!g_hash_table_size(conn->transfers)) gtk_widget_hide(conn->transfer_dialog); } static gboolean dialog_delete_cb(GtkWidget *widget, GdkEvent *event G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { gtk_dialog_response(GTK_DIALOG(widget), GTK_RESPONSE_CANCEL); return TRUE; } static void dialog_response_cb(GtkDialog *dialog, gint response_id, gpointer user_data) { spice_connection *conn = user_data; g_print("Reponse: %i\n", response_id); if (response_id == GTK_RESPONSE_CANCEL) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, conn->transfers); while (g_hash_table_iter_next(&iter, &key, &value)) { SpiceFileTransferTask *task = key; spice_file_transfer_task_cancel(task); } } } static void task_cancel_cb(GtkButton *button, gpointer user_data) { SpiceFileTransferTask *task = SPICE_FILE_TRANSFER_TASK(user_data); spice_file_transfer_task_cancel(task); } static TransferTaskWidgets * transfer_task_widgets_new(SpiceFileTransferTask *task) { char *filename; TransferTaskWidgets *widgets = g_new0(TransferTaskWidgets, 1); widgets->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); widgets->hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); widgets->cancel = gtk_button_new_with_label("Cancel"); widgets->progress = gtk_progress_bar_new(); filename = spice_file_transfer_task_get_filename(task); widgets->label = gtk_label_new(filename); g_free(filename); gtk_widget_set_halign(widgets->label, GTK_ALIGN_START); gtk_widget_set_valign(widgets->label, GTK_ALIGN_BASELINE); gtk_widget_set_valign(widgets->progress, GTK_ALIGN_CENTER); gtk_widget_set_hexpand(widgets->progress, TRUE); gtk_widget_set_valign(widgets->cancel, GTK_ALIGN_CENTER); gtk_widget_set_hexpand(widgets->progress, FALSE); gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->progress, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->cancel, FALSE, TRUE, 0); gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->label, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->hbox, TRUE, TRUE, 0); g_signal_connect(widgets->cancel, "clicked", G_CALLBACK(task_cancel_cb), task); gtk_widget_show_all(widgets->vbox); return widgets; } static void transfer_task_widgets_free(TransferTaskWidgets *widgets) { /* child widgets will be destroyed automatically */ gtk_widget_destroy(widgets->vbox); g_free(widgets); } static void spice_connection_add_task(spice_connection *conn, SpiceFileTransferTask *task) { TransferTaskWidgets *widgets; GtkWidget *content = NULL; g_signal_connect(task, "notify::progress", G_CALLBACK(transfer_update_progress), conn); g_signal_connect(task, "finished", G_CALLBACK(transfer_task_finished), conn); if (!conn->transfer_dialog) { conn->transfer_dialog = gtk_dialog_new_with_buttons("File Transfers", GTK_WINDOW(conn->wins[0]->toplevel), 0, "Cancel", GTK_RESPONSE_CANCEL, NULL); gtk_dialog_set_default_response(GTK_DIALOG(conn->transfer_dialog), GTK_RESPONSE_CANCEL); gtk_window_set_resizable(GTK_WINDOW(conn->transfer_dialog), FALSE); g_signal_connect(conn->transfer_dialog, "response", G_CALLBACK(dialog_response_cb), conn); g_signal_connect(conn->transfer_dialog, "delete-event", G_CALLBACK(dialog_delete_cb), conn); } gtk_widget_show(conn->transfer_dialog); content = gtk_dialog_get_content_area(GTK_DIALOG(conn->transfer_dialog)); gtk_container_set_border_width(GTK_CONTAINER(content), 12); widgets = transfer_task_widgets_new(task); g_hash_table_insert(conn->transfers, g_object_ref(task), widgets); gtk_box_pack_start(GTK_BOX(content), widgets->vbox, TRUE, TRUE, 6); } static void new_file_transfer(SpiceMainChannel *main, SpiceFileTransferTask *task, gpointer user_data) { spice_connection *conn = user_data; g_debug("new file transfer task"); spice_connection_add_task(conn, task); } static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data) { spice_connection *conn = data; int id; g_object_get(channel, "channel-id", &id, NULL); conn->channels++; SPICE_DEBUG("new channel (#%d)", id); if (SPICE_IS_MAIN_CHANNEL(channel)) { SPICE_DEBUG("new main channel"); conn->main = SPICE_MAIN_CHANNEL(channel); g_signal_connect(channel, "channel-event", G_CALLBACK(main_channel_event), conn); g_signal_connect(channel, "main-mouse-update", G_CALLBACK(main_mouse_update), conn); g_signal_connect(channel, "main-agent-update", G_CALLBACK(main_agent_update), conn); g_signal_connect(channel, "new-file-transfer", G_CALLBACK(new_file_transfer), conn); main_mouse_update(channel, conn); main_agent_update(channel, conn); } if (SPICE_IS_DISPLAY_CHANNEL(channel)) { if (id >= SPICE_N_ELEMENTS(conn->wins)) return; if (conn->wins[id] != NULL) return; SPICE_DEBUG("new display channel (#%d)", id); g_signal_connect(channel, "notify::monitors", G_CALLBACK(display_monitors), conn); spice_channel_connect(channel); } if (SPICE_IS_INPUTS_CHANNEL(channel)) { SPICE_DEBUG("new inputs channel"); g_signal_connect(channel, "inputs-modifiers", G_CALLBACK(inputs_modifiers), conn); } if (SPICE_IS_PLAYBACK_CHANNEL(channel)) { SPICE_DEBUG("new audio channel"); conn->audio = spice_audio_get(s, NULL); } if (SPICE_IS_USBREDIR_CHANNEL(channel)) { update_auto_usbredir_sensitive(conn); } if (SPICE_IS_PORT_CHANNEL(channel)) { g_signal_connect(channel, "notify::port-opened", G_CALLBACK(port_opened), conn); g_signal_connect(channel, "port-data", G_CALLBACK(port_data), conn); spice_channel_connect(channel); } } static void channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer data) { spice_connection *conn = data; int id; g_object_get(channel, "channel-id", &id, NULL); if (SPICE_IS_MAIN_CHANNEL(channel)) { SPICE_DEBUG("zap main channel"); conn->main = NULL; } if (SPICE_IS_DISPLAY_CHANNEL(channel)) { if (id >= SPICE_N_ELEMENTS(conn->wins)) return; SPICE_DEBUG("zap display channel (#%d)", id); /* FIXME destroy widget only */ } if (SPICE_IS_PLAYBACK_CHANNEL(channel)) { SPICE_DEBUG("zap audio channel"); } if (SPICE_IS_USBREDIR_CHANNEL(channel)) { update_auto_usbredir_sensitive(conn); } if (SPICE_IS_PORT_CHANNEL(channel)) { if (SPICE_PORT_CHANNEL(channel) == stdin_port) stdin_port = NULL; } conn->channels--; if (conn->channels > 0) { return; } connection_destroy(conn); } static void migration_state(GObject *session, GParamSpec *pspec, gpointer data) { SpiceSessionMigration mig; g_object_get(session, "migration-state", &mig, NULL); if (mig == SPICE_SESSION_MIGRATION_SWITCHING) g_message("migrating session"); } static spice_connection *connection_new(void) { spice_connection *conn; SpiceUsbDeviceManager *manager; conn = g_new0(spice_connection, 1); conn->session = spice_session_new(); conn->gtk_session = spice_gtk_session_get(conn->session); g_signal_connect(conn->session, "channel-new", G_CALLBACK(channel_new), conn); g_signal_connect(conn->session, "channel-destroy", G_CALLBACK(channel_destroy), conn); g_signal_connect(conn->session, "notify::migration-state", G_CALLBACK(migration_state), conn); manager = spice_usb_device_manager_get(conn->session, NULL); if (manager) { g_signal_connect(manager, "auto-connect-failed", G_CALLBACK(usb_connect_failed), NULL); g_signal_connect(manager, "device-error", G_CALLBACK(usb_connect_failed), NULL); } conn->transfers = g_hash_table_new_full(g_direct_hash, g_direct_equal, g_object_unref, (GDestroyNotify)transfer_task_widgets_free); connections++; SPICE_DEBUG("%s (%d)", __FUNCTION__, connections); return conn; } static void connection_connect(spice_connection *conn) { conn->disconnecting = false; spice_session_connect(conn->session); } static void connection_disconnect(spice_connection *conn) { if (conn->disconnecting) return; conn->disconnecting = true; spice_session_disconnect(conn->session); } static void connection_destroy(spice_connection *conn) { g_object_unref(conn->session); g_hash_table_unref(conn->transfers); free(conn); connections--; SPICE_DEBUG("%s (%d)", __FUNCTION__, connections); if (connections > 0) { return; } g_main_loop_quit(mainloop); } /* ------------------------------------------------------------------ */ static GOptionEntry cmd_entries[] = { { .long_name = "full-screen", .short_name = 'f', .arg = G_OPTION_ARG_NONE, .arg_data = &fullscreen, .description = "Open in full screen mode", },{ .long_name = "version", .arg = G_OPTION_ARG_NONE, .arg_data = &version, .description = "Display version and quit", },{ .long_name = "title", .arg = G_OPTION_ARG_STRING, .arg_data = &spicy_title, .description = "Set the window title", .arg_description = "", },{ /* end of list */ } }; static void usb_connect_failed(GObject *object, SpiceUsbDevice *device, GError *error, gpointer data) { GtkWidget *dialog; if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_CANCELLED) return; dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "USB redirection error"); gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error->message); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); } static void setup_terminal(gboolean reset) { int stdinfd = fileno(stdin); if (!isatty(stdinfd)) return; #ifdef HAVE_TERMIOS_H struct termios tios; static struct termios saved_tios; static bool saved = false; if (reset) { if (!saved) return; tios = saved_tios; } else { tcgetattr(stdinfd, &tios); saved_tios = tios; saved = true; tios.c_lflag &= ~(ICANON | ECHO); } tcsetattr(stdinfd, TCSANOW, &tios); #endif } static void watch_stdin(void) { int stdinfd = fileno(stdin); GIOChannel *gin; setup_terminal(false); gin = g_io_channel_unix_new(stdinfd); g_io_channel_set_flags(gin, G_IO_FLAG_NONBLOCK, NULL); g_io_add_watch(gin, G_IO_IN|G_IO_ERR|G_IO_HUP|G_IO_NVAL, input_cb, NULL); } int main(int argc, char *argv[]) { GError *error = NULL; GOptionContext *context; spice_connection *conn; gchar *conf_file, *conf; char *host = NULL, *port = NULL, *tls_port = NULL, *unix_path = NULL; keyfile = g_key_file_new(); int mode = S_IRWXU; conf_file = g_build_filename(g_get_user_config_dir(), "spicy", NULL); if (g_mkdir_with_parents(conf_file, mode) == -1) SPICE_DEBUG("failed to create config directory"); g_free(conf_file); conf_file = g_build_filename(g_get_user_config_dir(), "spicy", "settings", NULL); if (!g_key_file_load_from_file(keyfile, conf_file, G_KEY_FILE_KEEP_COMMENTS|G_KEY_FILE_KEEP_TRANSLATIONS, &error)) { SPICE_DEBUG("Couldn't load configuration: %s", error->message); g_clear_error(&error); } /* parse opts */ gtk_init(&argc, &argv); context = g_option_context_new("- spice client test application"); g_option_context_set_summary(context, "Gtk+ test client to connect to Spice servers."); g_option_context_set_description(context, "Report bugs to " PACKAGE_BUGREPORT "."); g_option_context_add_group(context, spice_get_option_group()); g_option_context_set_main_group(context, spice_cmdline_get_option_group()); g_option_context_add_main_entries(context, cmd_entries, NULL); g_option_context_add_group(context, gtk_get_option_group(TRUE)); if (!g_option_context_parse (context, &argc, &argv, &error)) { g_print("option parsing failed: %s\n", error->message); exit(1); } g_option_context_free(context); if (version) { g_print("spicy " PACKAGE_VERSION "\n"); exit(0); } mainloop = g_main_loop_new(NULL, false); conn = connection_new(); spice_set_session_option(conn->session); spice_cmdline_session_setup(conn->session); g_object_get(conn->session, "unix-path", &unix_path, "host", &host, "port", &port, "tls-port", &tls_port, NULL); /* If user doesn't provide hostname and port, show the dialog window instead of connecting to server automatically */ if ((host == NULL || (port == NULL && tls_port == NULL)) && unix_path == NULL) { if (!spicy_connect_dialog(conn->session)) { exit(0); } } g_free(host); g_free(port); g_free(tls_port); g_free(unix_path); connection_connect(conn); if (connections > 0) g_main_loop_run(mainloop); g_main_loop_unref(mainloop); if ((conf = g_key_file_to_data(keyfile, NULL, &error)) == NULL || !g_file_set_contents(conf_file, conf, -1, &error)) { SPICE_DEBUG("Couldn't save configuration: %s", error->message); g_error_free(error); error = NULL; } g_free(conf_file); g_free(conf); g_key_file_free(keyfile); g_free(spicy_title); setup_terminal(true); return 0; }