/* -*- 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 .
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include
#include
#ifdef HAVE_TERMIOS_H
#include
#endif
#define GNOME_DESKTOP_USE_UNSTABLE_API 2
#include "display/gnome-rr.h"
#include "display/gnome-rr-config.h"
#ifdef USE_SMARTCARD
#include
#include "smartcard-manager.h"
#endif
#include "glib-compat.h"
#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"
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 WIN32
gint win_x;
gint win_y;
#endif
bool enable_accels_save;
bool enable_mnemonics_save;
};
struct _SpiceWindowClass
{
GObjectClass parent_class;
};
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;
};
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 resolution_fullscreen(SpiceWindow *win);
static void resolution_restore(SpiceWindow *win);
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 GnomeRRScreen *rrscreen = NULL;
static GnomeRRConfig *rrsaved = NULL;
static GnomeRRConfig *rrcurrent = 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,
GTK_STOCK_OK,
GTK_RESPONSE_ACCEPT,
GTK_STOCK_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 struct {
const char *text;
const char *prop;
GtkWidget *entry;
} connect_entries[] = {
{ .text = N_("Hostname"), .prop = "host" },
{ .text = N_("Port"), .prop = "port" },
{ .text = N_("TLS Port"), .prop = "tls-port" },
};
#ifndef WIN32
static void recent_selection_changed_dialog_cb(GtkRecentChooser *chooser, gpointer data)
{
GtkRecentInfo *info;
gchar *txt = NULL;
const gchar *uri;
SpiceSession *session = data;
info = gtk_recent_chooser_get_current_item(chooser);
if (info == NULL)
return;
uri = gtk_recent_info_get_uri(info);
g_return_if_fail(uri != NULL);
g_object_set(session, "uri", uri, NULL);
g_object_get(session, "host", &txt, NULL);
gtk_entry_set_text(GTK_ENTRY(connect_entries[0].entry), txt ? txt : "");
g_free(txt);
g_object_get(session, "port", &txt, NULL);
gtk_entry_set_text(GTK_ENTRY(connect_entries[1].entry), txt ? txt : "");
g_free(txt);
g_object_get(session, "tls-port", &txt, NULL);
gtk_entry_set_text(GTK_ENTRY(connect_entries[2].entry), txt ? txt : "");
g_free(txt);
gtk_recent_info_unref(info);
}
static void recent_item_activated_dialog_cb(GtkRecentChooser *chooser, gpointer data)
{
gtk_dialog_response (GTK_DIALOG (data), GTK_RESPONSE_ACCEPT);
}
#endif
static int connect_dialog(SpiceSession *session)
{
GtkWidget *dialog, *area, *label;
GtkTable *table;
int i, retval;
/* Create the widgets */
dialog = gtk_dialog_new_with_buttons(_("Connect to SPICE"),
NULL,
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_STOCK_CANCEL,
GTK_RESPONSE_REJECT,
GTK_STOCK_CONNECT,
GTK_RESPONSE_ACCEPT,
NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
table = GTK_TABLE(gtk_table_new(3, 2, 0));
gtk_box_pack_start(GTK_BOX(area), GTK_WIDGET(table), TRUE, TRUE, 0);
gtk_table_set_row_spacings(table, 5);
gtk_table_set_col_spacings(table, 5);
for (i = 0; i < SPICE_N_ELEMENTS(connect_entries); i++) {
gchar *txt;
label = gtk_label_new(connect_entries[i].text);
gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
gtk_table_attach_defaults(table, label, 0, 1, i, i+1);
connect_entries[i].entry = GTK_WIDGET(gtk_entry_new());
gtk_table_attach_defaults(table, connect_entries[i].entry, 1, 2, i, i+1);
g_object_get(session, connect_entries[i].prop, &txt, NULL);
SPICE_DEBUG("%s: #%i [%s]: \"%s\"",
__FUNCTION__, i, connect_entries[i].prop, txt);
if (txt) {
gtk_entry_set_text(GTK_ENTRY(connect_entries[i].entry), txt);
g_free(txt);
}
}
label = gtk_label_new("Recent connections:");
gtk_box_pack_start(GTK_BOX(area), label, TRUE, TRUE, 0);
gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
#ifndef WIN32
GtkRecentFilter *rfilter;
GtkWidget *recent;
recent = GTK_WIDGET(gtk_recent_chooser_widget_new());
gtk_recent_chooser_set_show_icons(GTK_RECENT_CHOOSER(recent), FALSE);
gtk_box_pack_start(GTK_BOX(area), recent, TRUE, TRUE, 0);
rfilter = gtk_recent_filter_new();
gtk_recent_filter_add_mime_type(rfilter, "application/x-spice");
gtk_recent_chooser_set_filter(GTK_RECENT_CHOOSER(recent), rfilter);
gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(recent), FALSE);
g_signal_connect(recent, "selection-changed",
G_CALLBACK(recent_selection_changed_dialog_cb), session);
g_signal_connect(recent, "item-activated",
G_CALLBACK(recent_item_activated_dialog_cb), dialog);
#endif
/* show and wait for response */
gtk_widget_show_all(dialog);
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
for (i = 0; i < SPICE_N_ELEMENTS(connect_entries); i++) {
const gchar *txt;
txt = gtk_entry_get_text(GTK_ENTRY(connect_entries[i].entry));
g_object_set(session, connect_entries[i].prop, txt, NULL);
}
retval = 0;
} else
retval = -1;
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 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 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
#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,
GTK_STOCK_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", GTK_STOCK_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 = "HelpMenu",
.label = "_Help",
},{
/* File menu */
.name = "Connect",
.stock_id = GTK_STOCK_CONNECT,
.label = N_("_Connect ..."),
.callback = G_CALLBACK(menu_cb_connect),
},{
.name = "Close",
.stock_id = GTK_STOCK_CLOSE,
.label = N_("_Close"),
.callback = G_CALLBACK(menu_cb_close),
.accelerator = "", /* none (disable default "W") */
},{
/* Edit menu */
.name = "CopyToGuest",
.stock_id = GTK_STOCK_COPY,
.label = N_("_Copy to guest"),
.callback = G_CALLBACK(menu_cb_copy),
.accelerator = "", /* none (disable default "C") */
},{
.name = "PasteFromGuest",
.stock_id = GTK_STOCK_PASTE,
.label = N_("_Paste from guest"),
.callback = G_CALLBACK(menu_cb_paste),
.accelerator = "", /* none (disable default "V") */
},{
/* View menu */
.name = "Fullscreen",
.stock_id = GTK_STOCK_FULLSCREEN,
.label = N_("_Fullscreen"),
.callback = G_CALLBACK(menu_cb_fullscreen),
.accelerator = "F11",
},{
#ifdef USE_SMARTCARD
.name = "InsertSmartcard",
.label = N_("_Insert Smartcard"),
.callback = G_CALLBACK(menu_cb_insert_smartcard),
.accelerator = "F8",
},{
.name = "RemoveSmartcard",
.label = N_("_Remove Smartcard"),
.callback = G_CALLBACK(menu_cb_remove_smartcard),
.accelerator = "F9",
},{
#endif
#ifdef USE_USBREDIR
.name = "SelectUsbDevices",
.label = N_("_Select USB Devices for redirection"),
.callback = G_CALLBACK(menu_cb_select_usb_devices),
.accelerator = "F10",
},{
#endif
/* Help menu */
.name = "About",
.stock_id = GTK_STOCK_ABOUT,
.label = N_("_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",
};
static const GtkToggleActionEntry tentries[] = {
{
.name = "grab-keyboard",
.label = N_("Grab keyboard when active and focused"),
.callback = G_CALLBACK(menu_cb_bool_prop),
},{
.name = "grab-mouse",
.label = N_("Grab mouse in server mode (no tabled/vdagent)"),
.callback = G_CALLBACK(menu_cb_bool_prop),
},{
.name = "resize-guest",
.label = N_("Resize guest to match window size"),
.callback = G_CALLBACK(menu_cb_bool_prop),
},{
.name = "scaling",
.label = N_("Scale display"),
.callback = G_CALLBACK(menu_cb_bool_prop),
},{
.name = "disable-inputs",
.label = N_("Disable inputs"),
.callback = G_CALLBACK(menu_cb_bool_prop),
},{
.name = "auto-clipboard",
.label = N_("Automagic clipboard sharing between host and guest"),
.callback = G_CALLBACK(menu_cb_bool_prop),
},{
.name = "auto-usbredir",
.label = N_("Auto redirect newly plugged in USB devices"),
.callback = G_CALLBACK(menu_cb_bool_prop),
},{
.name = "Statusbar",
.label = N_("Statusbar"),
.callback = G_CALLBACK(menu_cb_statusbar),
},{
.name = "Toolbar",
.label = N_("Toolbar"),
.callback = G_CALLBACK(menu_cb_toolbar),
}
};
static char ui_xml[] =
"\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"
" \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;
}
#ifndef WIN32
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);
}
#endif
static GnomeRROutputInfo *
get_nearest_output (GnomeRRConfig *configuration, int x, int y)
{
int i;
int nearest_index;
int nearest_dist;
GnomeRROutputInfo **outputs;
nearest_index = -1;
nearest_dist = G_MAXINT;
outputs = gnome_rr_config_get_outputs (configuration);
for (i = 0; outputs[i] != NULL; i++)
{
int dist_x, dist_y;
int output_x, output_y, output_width, output_height;
if (!(gnome_rr_output_info_is_connected (outputs[i]) && gnome_rr_output_info_is_active (outputs[i])))
continue;
gnome_rr_output_info_get_geometry (outputs[i], &output_x, &output_y, &output_width, &output_height);
if (x < output_x)
dist_x = output_x - x;
else if (x >= output_x + output_width)
dist_x = x - (output_x + output_width) + 1;
else
dist_x = 0;
if (y < output_y)
dist_y = output_y - y;
else if (y >= output_y + output_height)
dist_y = y - (output_y + output_height) + 1;
else
dist_y = 0;
if (MIN (dist_x, dist_y) < nearest_dist)
{
nearest_dist = MIN (dist_x, dist_y);
nearest_index = i;
}
}
if (nearest_index != -1)
return outputs[nearest_index];
else
return NULL;
}
#if !GTK_CHECK_VERSION (2, 91, 0)
#define gdk_window_get_geometry(win,x,y,w,h) gdk_window_get_geometry(win,x,y,w,h,NULL)
#endif
static GnomeRROutputInfo *
get_output_for_window(GnomeRRConfig *configuration, GdkWindow *window)
{
GdkRectangle win_rect;
int i;
int largest_area;
int largest_index;
GnomeRROutputInfo **outputs;
gdk_window_get_geometry (window, &win_rect.x, &win_rect.y, &win_rect.width, &win_rect.height);
gdk_window_get_origin (window, &win_rect.x, &win_rect.y);
largest_area = 0;
largest_index = -1;
outputs = gnome_rr_config_get_outputs (configuration);
for (i = 0; outputs[i] != NULL; i++)
{
GdkRectangle output_rect, intersection;
gnome_rr_output_info_get_geometry (outputs[i], &output_rect.x, &output_rect.y, &output_rect.width, &output_rect.height);
if (gnome_rr_output_info_is_connected (outputs[i]) && gdk_rectangle_intersect (&win_rect, &output_rect, &intersection))
{
int area;
area = intersection.width * intersection.height;
if (area > largest_area)
{
largest_area = area;
largest_index = i;
}
}
}
if (largest_index != -1)
return outputs[largest_index];
else
return get_nearest_output (configuration,
win_rect.x + win_rect.width / 2,
win_rect.y + win_rect.height / 2);
}
static void
on_screen_changed(GnomeRRScreen *scr, gpointer data)
{
GError *error = NULL;
GnomeRRConfig *current;
current = gnome_rr_config_new_current(rrscreen, &error);
if (!current) {
g_warning("Can't get current display config: %s", error->message);
goto end;
}
if (rrcurrent)
g_object_unref(rrcurrent);
rrcurrent = current;
end:
g_clear_error(&error);
}
static void resolution_fullscreen(SpiceWindow *win)
{
GnomeRROutputInfo *output;
int x, y, width, height;
GError *error = NULL;;
if (!rrsaved) {
rrsaved = gnome_rr_config_new_current(rrscreen, &error);
g_clear_error(&error);
}
output = get_output_for_window(rrcurrent, gtk_widget_get_window(win->spice));
g_return_if_fail(output != NULL);
gnome_rr_output_info_get_geometry (output, &x, &y, &width, &height);
g_object_get(win->display_channel, "width", &width, "height", &height, NULL);
gnome_rr_output_info_set_geometry (output, x, y, width, height);
if (!gnome_rr_config_apply_with_time(rrcurrent, rrscreen,
gtk_get_current_event_time (), &error)) {
g_warning("Can't set display config: %s", error->message);
}
g_clear_error(&error);
#ifdef WIN32
/* recenter the window on Windows */
gtk_window_fullscreen(GTK_WINDOW(win->toplevel));
#endif
}
static void resolution_restore(SpiceWindow *win)
{
GnomeRROutputInfo *output, *saved;
int x, y, width, height;
GError *error = NULL;;
if (rrsaved == NULL)
return;
output = get_output_for_window(rrcurrent, gtk_widget_get_window(win->spice));
g_return_if_fail(output != NULL);
saved = get_output_for_window(rrsaved, gtk_widget_get_window(win->spice));
g_return_if_fail(saved != NULL);
gnome_rr_output_info_get_geometry (saved, &x, &y, &width, &height);
gnome_rr_output_info_set_geometry (output, x, y, width, height);
if (!gnome_rr_config_apply_with_time(rrcurrent, rrscreen,
gtk_get_current_event_time (), &error)) {
g_warning("Can't set display config: %s", error->message);
}
g_clear_error(&error);
#ifdef WIN32
/* recenter the window on Windows */
gtk_window_unfullscreen(GTK_WINDOW(win->toplevel));
#endif
}
static void resolution_restore_all(void)
{
GError *error = NULL;;
if (!rrsaved)
return;
if (!gnome_rr_config_apply_with_time(rrsaved, rrscreen,
gtk_get_current_event_time (), &error)) {
g_warning("Can't restore display config: %s", error->message);
}
g_clear_error(&error);
g_object_unref(rrsaved);
rrsaved = NULL;
}
static gboolean configure_event_cb(GtkWidget *widget,
GdkEventConfigure *event,
gpointer data)
{
gboolean resize_guest;
SpiceWindow *win = data;
g_return_val_if_fail(win != NULL, FALSE);
g_return_val_if_fail(win->conn != NULL, FALSE);
g_object_get(win->spice, "resize-guest", &resize_guest, NULL);
if (resize_guest && win->conn->agent_connected)
return FALSE;
if (win->fullscreen)
resolution_fullscreen(win);
else
resolution_restore(win);
return FALSE;
}
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_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");
#ifndef WIN32
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);
#endif
/* spice display */
win->spice = GTK_WIDGET(spice_display_new_with_monitor(conn->session, id, monitor_id));
g_signal_connect(win->spice, "configure-event", G_CALLBACK(configure_event_cb), win);
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 */
#if GTK_CHECK_VERSION(3,0,0)
win->statusbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1);
#else
win->statusbar = gtk_hbox_new(FALSE, 1);
#endif
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 */
#if GTK_CHECK_VERSION(3,0,0)
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1);
#else
vbox = gtk_vbox_new(FALSE, 1);
#endif
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);
g_return_if_fail(g_str_has_prefix(uri, "spice://"));
recent = gtk_recent_manager_get_default();
meta.display_name = uri + 8;
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)
{
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:
g_message("main channel: failed to connect");
rc = connect_dialog(conn->session);
if (rc == 0) {
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: %d", 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 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);
}
/* handle the first spicy port and connect it to stdin/out */
if (g_strcmp0(name, "org.spice.spicy") == 0 && stdin_port == NULL) {
stdin_port = port;
goto end;
}
if (port == stdin_port)
goto end;
spice_channel_flush_async(channel, NULL, port_flushed_cb, conn);
} else {
if (port == stdin_port)
stdin_port = NULL;
}
end:
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);
}
}
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);
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);
}
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);
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 = N_("Open in full screen mode"),
},{
.long_name = "version",
.arg = G_OPTION_ARG_NONE,
.arg_data = &version,
.description = N_("Display version and quit"),
},{
.long_name = "title",
.arg = G_OPTION_ARG_STRING,
.arg_data = &spicy_title,
.description = N_("Set the window title"),
.arg_description = N_(""),
},{
/* end of list */
}
};
static void (* segv_handler) (int) = SIG_DFL;
static void (* abrt_handler) (int) = SIG_DFL;
static void (* fpe_handler) (int) = SIG_DFL;
static void (* ill_handler) (int) = SIG_DFL;
#ifndef WIN32
static void (* bus_handler) (int) = SIG_DFL;
#endif
static void
signal_handler(int signum)
{
static gint recursion = FALSE;
/*
* reset all signal handlers: any further crashes should just be allowed
* to crash normally.
* */
signal(SIGSEGV, segv_handler);
signal(SIGABRT, abrt_handler);
signal(SIGFPE, fpe_handler);
signal(SIGILL, ill_handler);
#ifndef WIN32
signal(SIGBUS, bus_handler);
#endif
/* Stop bizarre loops */
if (recursion)
abort ();
recursion = TRUE;
g_main_loop_quit(mainloop);
}
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
static struct termios saved_tios;
struct termios tios;
if (reset)
tios = saved_tios;
else {
tcgetattr(stdinfd, &tios);
saved_tios = tios;
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;
#if !GLIB_CHECK_VERSION(2,31,18)
g_thread_init(NULL);
#endif
bindtextdomain(GETTEXT_PACKAGE, SPICE_GTK_LOCALEDIR);
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
textdomain(GETTEXT_PACKAGE);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
segv_handler = signal(SIGSEGV, signal_handler);
abrt_handler = signal(SIGABRT, signal_handler);
fpe_handler = signal(SIGFPE, signal_handler);
ill_handler = signal(SIGILL, signal_handler);
#ifndef WIN32
signal(SIGHUP, signal_handler);
bus_handler = signal(SIGBUS, signal_handler);
#endif
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);
}
g_type_init();
mainloop = g_main_loop_new(NULL, false);
rrscreen = gnome_rr_screen_new(gdk_screen_get_default (), &error);
g_warn_if_fail(rrscreen != NULL);
if (rrscreen)
g_signal_connect(rrscreen, "changed", G_CALLBACK(on_screen_changed), NULL);
on_screen_changed(rrscreen, NULL);
conn = connection_new();
spice_set_session_option(conn->session);
spice_cmdline_session_setup(conn->session);
g_object_get(conn->session,
"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)) {
int ret = connect_dialog(conn->session);
if (ret != 0) {
exit(0);
}
}
g_free(host);
g_free(port);
g_free(tls_port);
watch_stdin();
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;
}
resolution_restore_all();
g_object_unref(rrscreen);
g_free(conf_file);
g_free(conf);
g_key_file_free(keyfile);
g_free(spicy_title);
setup_terminal(true);
return 0;
}