/* Copyright (C) 2012 Red Hat, Inc. Red Hat Authors: Hans de Goede 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 "spice-client.h" #include "spice-marshal.h" #include "usb-device-widget.h" #include "usb-device-manager.h" /** * SECTION:usb-device-widget * @short_description: USB device selection widget * @title: Spice USB device selection widget * @section_id: * @see_also: * @stability: Stable * @include: spice-client-gtk.h * * #SpiceUsbDeviceWidget is a gtk widget which apps can use to easily * add an UI to select USB devices to redirect (or unredirect). */ struct _SpiceUsbDeviceWidget { GtkBox parent; SpiceUsbDeviceWidgetPrivate *priv; }; struct _SpiceUsbDeviceWidgetClass { GtkBoxClass parent_class; /* signals */ void (*connect_failed) (SpiceUsbDeviceWidget *widget, SpiceUsbDevice *device, GError *error); }; /* ------------------------------------------------------------------ */ /* Prototypes for callbacks */ static void device_added_cb(SpiceUsbDeviceManager *manager, SpiceUsbDevice *device, gpointer user_data); static void device_removed_cb(SpiceUsbDeviceManager *manager, SpiceUsbDevice *device, gpointer user_data); static void device_error_cb(SpiceUsbDeviceManager *manager, SpiceUsbDevice *device, GError *err, gpointer user_data); static void empty_cd_clicked_cb(GtkToggleButton *toggle, gpointer user_data); static gboolean spice_usb_device_widget_update_status(gpointer user_data); enum { PROP_0, PROP_SESSION, PROP_DEVICE_FORMAT_STRING, }; enum { CONNECT_FAILED, LAST_SIGNAL, }; struct _SpiceUsbDeviceWidgetPrivate { SpiceSession *session; gchar *device_format_string; SpiceUsbDeviceManager *manager; GtkWidget *empty_cd; GtkWidget *info_bar; GtkWidget *label; gchar *err_msg; gsize device_count; }; static guint signals[LAST_SIGNAL] = { 0, }; G_DEFINE_TYPE_WITH_PRIVATE(SpiceUsbDeviceWidget, spice_usb_device_widget, GTK_TYPE_BOX) static void spice_usb_device_widget_get_property(GObject *gobject, guint prop_id, GValue *value, GParamSpec *pspec) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject); SpiceUsbDeviceWidgetPrivate *priv = self->priv; switch (prop_id) { case PROP_SESSION: g_value_set_object(value, priv->session); break; case PROP_DEVICE_FORMAT_STRING: g_value_set_string(value, priv->device_format_string); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec); break; } } static void spice_usb_device_widget_set_property(GObject *gobject, guint prop_id, const GValue *value, GParamSpec *pspec) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject); SpiceUsbDeviceWidgetPrivate *priv = self->priv; switch (prop_id) { case PROP_SESSION: priv->session = g_value_dup_object(value); break; case PROP_DEVICE_FORMAT_STRING: priv->device_format_string = g_value_dup_string(value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec); break; } } static void spice_usb_device_widget_add_err_msg(SpiceUsbDeviceWidget *self, gchar *new_err_msg) { SpiceUsbDeviceWidgetPrivate *priv = self->priv; if (priv->err_msg) { /* Append the new error message to err_msg, but only if it's *not* already there! */ if (!strstr(priv->err_msg, new_err_msg)) { gchar *old_err_msg = priv->err_msg; priv->err_msg = g_strdup_printf("%s\n%s", old_err_msg, new_err_msg); g_free(old_err_msg); } g_free(new_err_msg); } else { priv->err_msg = new_err_msg; } } static void spice_usb_device_widget_hide_info_bar(SpiceUsbDeviceWidget *self) { SpiceUsbDeviceWidgetPrivate *priv = self->priv; g_clear_pointer(&priv->info_bar, gtk_widget_destroy); } static void spice_usb_device_widget_show_info_bar(SpiceUsbDeviceWidget *self, const gchar *message, GtkMessageType message_type, const gchar *stock_icon_id) { SpiceUsbDeviceWidgetPrivate *priv = self->priv; GtkWidget *info_bar, *content_area, *hbox, *widget; spice_usb_device_widget_hide_info_bar(self); info_bar = gtk_info_bar_new(); gtk_info_bar_set_message_type(GTK_INFO_BAR(info_bar), message_type); content_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_bar)); hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); gtk_container_add(GTK_CONTAINER(content_area), hbox); widget = gtk_image_new_from_icon_name(stock_icon_id, GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0); widget = gtk_label_new(message); gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0); priv->info_bar = info_bar; gtk_widget_set_margin_start(info_bar, 12); gtk_widget_set_halign(info_bar, GTK_ALIGN_FILL); gtk_box_pack_start(GTK_BOX(self), priv->info_bar, FALSE, FALSE, 0); gtk_widget_show_all(priv->info_bar); } static void empty_cd_clicked_cb(GtkToggleButton *toggle, gpointer user_data) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data); SpiceUsbDeviceWidgetPrivate *priv = self->priv; GtkWidget *dialog; gint dialog_rc; if (!gtk_toggle_button_get_active(toggle)) { return; } gtk_toggle_button_set_active(toggle, FALSE); dialog = gtk_file_chooser_dialog_new(_("Select ISO file or device"), GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))), GTK_FILE_CHOOSER_ACTION_OPEN, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Open"), GTK_RESPONSE_ACCEPT, NULL); dialog_rc = gtk_dialog_run(GTK_DIALOG(dialog)); if (dialog_rc == GTK_RESPONSE_ACCEPT) { gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); GError *err = NULL; gboolean rc; rc = spice_usb_device_manager_create_shared_cd_device(priv->manager, filename, &err); if (!rc && err != NULL) { const gchar *basename = g_path_get_basename(filename); gchar *err_msg = g_strdup_printf(_("shared CD %s, %s"), basename, err->message); g_free((gpointer)basename); SPICE_DEBUG("Failed to create %s", err_msg); spice_usb_device_widget_add_err_msg(self, err_msg); spice_usb_device_widget_update_status(user_data); g_clear_error(&err); } } gtk_widget_destroy(dialog); } static void spice_usb_device_widget_add_empty_cd(SpiceUsbDeviceWidget *self) { SpiceUsbDeviceWidgetPrivate *priv = self->priv; GtkWidget *empty_cd, *cd_label; empty_cd = gtk_check_button_new_with_label(_("SPICE CD (empty)")); cd_label = gtk_bin_get_child(GTK_BIN(empty_cd)); gtk_label_set_ellipsize(GTK_LABEL(cd_label), PANGO_ELLIPSIZE_MIDDLE); g_signal_connect(G_OBJECT(empty_cd), "toggled", G_CALLBACK(empty_cd_clicked_cb), self); gtk_widget_set_margin_start(empty_cd, 12); gtk_box_pack_end(GTK_BOX(self), empty_cd, FALSE, FALSE, 0); gtk_widget_show_all(empty_cd); priv->empty_cd = empty_cd; } static void spice_usb_device_widget_constructed(GObject *gobject) { SpiceUsbDeviceWidget *self; SpiceUsbDeviceWidgetPrivate *priv; GPtrArray *devices = NULL; GError *err = NULL; gchar *str; self = SPICE_USB_DEVICE_WIDGET(gobject); priv = self->priv; if (!priv->session) { g_error("SpiceUsbDeviceWidget constructed without a session"); } priv->label = gtk_label_new(NULL); str = g_strdup_printf("%s", _("Select USB devices to redirect")); gtk_label_set_markup(GTK_LABEL (priv->label), str); g_free(str); gtk_label_set_xalign(GTK_LABEL(priv->label), 0.0); gtk_label_set_yalign(GTK_LABEL(priv->label), 0.5); gtk_box_pack_start(GTK_BOX(self), priv->label, FALSE, FALSE, 0); priv->manager = spice_usb_device_manager_get(priv->session, &err); if (err) { spice_usb_device_widget_show_info_bar(self, err->message, GTK_MESSAGE_WARNING, "dialog-warning"); g_clear_error(&err); return; } g_signal_connect(priv->manager, "device-added", G_CALLBACK(device_added_cb), self); g_signal_connect(priv->manager, "device-removed", G_CALLBACK(device_removed_cb), self); g_signal_connect(priv->manager, "device-error", G_CALLBACK(device_error_cb), self); spice_usb_device_widget_add_empty_cd(self); devices = spice_usb_device_manager_get_devices(priv->manager); if (devices != NULL) { int i; for (i = 0; i < devices->len; i++) { device_added_cb(NULL, g_ptr_array_index(devices, i), self); } g_ptr_array_unref(devices); } spice_usb_device_widget_update_status(self); } static void spice_usb_device_widget_finalize(GObject *object) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(object); SpiceUsbDeviceWidgetPrivate *priv = self->priv; if (priv->manager) { g_signal_handlers_disconnect_by_func(priv->manager, device_added_cb, self); g_signal_handlers_disconnect_by_func(priv->manager, device_removed_cb, self); g_signal_handlers_disconnect_by_func(priv->manager, device_error_cb, self); } g_object_unref(priv->session); g_free(priv->device_format_string); if (G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize) { G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize(object); } } static void spice_usb_device_widget_class_init(SpiceUsbDeviceWidgetClass *klass) { GObjectClass *gobject_class = (GObjectClass *)klass; GParamSpec *pspec; gobject_class->constructed = spice_usb_device_widget_constructed; gobject_class->finalize = spice_usb_device_widget_finalize; gobject_class->get_property = spice_usb_device_widget_get_property; gobject_class->set_property = spice_usb_device_widget_set_property; /** * SpiceUsbDeviceWidget:session: * * #SpiceSession this #SpiceUsbDeviceWidget is associated with * **/ pspec = g_param_spec_object("session", "Session", "SpiceSession", SPICE_TYPE_SESSION, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property(gobject_class, PROP_SESSION, pspec); /** * SpiceUsbDeviceWidget:device-format-string: * * Format string to pass to spice_usb_device_get_description() for getting * the device USB descriptions. */ pspec = g_param_spec_string("device-format-string", "Device format string", "Format string for device description", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property(gobject_class, PROP_DEVICE_FORMAT_STRING, pspec); /** * SpiceUsbDeviceWidget::connect-failed: * @widget: The #SpiceUsbDeviceWidget that emitted the signal * @device: #SpiceUsbDevice boxed object corresponding to the added device * @error: #GError describing the reason why the connect failed * * The #SpiceUsbDeviceWidget::connect-failed signal is emitted whenever * the user has requested for a device to be redirected and this has * failed. **/ signals[CONNECT_FAILED] = g_signal_new("connect-failed", G_OBJECT_CLASS_TYPE(gobject_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(SpiceUsbDeviceWidgetClass, connect_failed), NULL, NULL, g_cclosure_user_marshal_VOID__BOXED_BOXED, G_TYPE_NONE, 2, SPICE_TYPE_USB_DEVICE, G_TYPE_ERROR); } static void spice_usb_device_widget_init(SpiceUsbDeviceWidget *self) { self->priv = spice_usb_device_widget_get_instance_private(self); } /* ------------------------------------------------------------------ */ /* public api */ /** * spice_usb_device_widget_new: * @session: #SpiceSession for which to widget will control USB redirection * @device_format_string: (allow-none): String passed to * spice_usb_device_get_description() * * Creates a new widget to control USB redirection. * * Returns: a new #SpiceUsbDeviceWidget instance */ GtkWidget *spice_usb_device_widget_new(SpiceSession *session, const gchar *device_format_string) { return g_object_new(SPICE_TYPE_USB_DEVICE_WIDGET, "orientation", GTK_ORIENTATION_VERTICAL, "session", session, "device-format-string", device_format_string, "spacing", 6, NULL); } /* ------------------------------------------------------------------ */ /* callbacks */ static SpiceUsbDevice *get_usb_device(GtkWidget *widget) { return g_object_get_data(G_OBJECT(widget), "usb-device"); } static void check_can_redirect(GtkWidget *widget, gpointer user_data) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data); SpiceUsbDeviceWidgetPrivate *priv = self->priv; SpiceUsbDevice *device; gboolean can_redirect; GError *err = NULL; device = get_usb_device(widget); if (!device) { return; /* Non device widget, ie the info_bar */ } priv->device_count++; if (spice_usb_device_manager_is_redirecting(priv->manager)) { can_redirect = FALSE; } else { can_redirect = spice_usb_device_manager_can_redirect_device(priv->manager, device, &err); if (!can_redirect) { gchar *err_msg = g_strdup_printf(_("Can't redirect: %s"), err->message); spice_usb_device_widget_add_err_msg(self, err_msg); } g_clear_error(&err); } gtk_widget_set_sensitive(widget, can_redirect); } static gboolean spice_usb_device_widget_update_status(gpointer user_data) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data); SpiceUsbDeviceWidgetPrivate *priv = self->priv; gchar *str, *markup_str; const gchar *free_channels_str; int free_channels; gboolean redirecting; redirecting = spice_usb_device_manager_is_redirecting(priv->manager); g_object_get(priv->manager, "free-channels", &free_channels, NULL); free_channels_str = g_dngettext(GETTEXT_PACKAGE, "Select USB devices to redirect (%d free channel)", "Select USB devices to redirect (%d free channels)", free_channels); str = g_strdup_printf(free_channels_str, free_channels); markup_str = g_strdup_printf("%s", str); gtk_label_set_markup(GTK_LABEL (priv->label), markup_str); g_free(markup_str); g_free(str); priv->device_count = 0; gtk_container_foreach(GTK_CONTAINER(self), check_can_redirect, self); if (priv->err_msg) { spice_usb_device_widget_show_info_bar(self, priv->err_msg, GTK_MESSAGE_INFO, "dialog-warning"); g_clear_pointer(&priv->err_msg, g_free); } else if (redirecting) { spice_usb_device_widget_show_info_bar(self, _("Redirecting USB Device..."), GTK_MESSAGE_INFO, "dialog-information"); } else { spice_usb_device_widget_hide_info_bar(self); } if (priv->device_count == 0) { spice_usb_device_widget_show_info_bar(self, _("No USB devices detected"), GTK_MESSAGE_INFO, "dialog-information"); } return FALSE; } typedef struct _connect_cb_data { GtkWidget *check; SpiceUsbDeviceWidget *self; } connect_cb_data; static void connect_cb_data_free(connect_cb_data *data) { spice_usb_device_widget_update_status(data->self); g_object_unref(data->check); g_object_unref(data->self); g_free(data); } static void _disconnect_cb(GObject *gobject, GAsyncResult *res, gpointer user_data) { SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(gobject); connect_cb_data *data = user_data; GError *err = NULL; spice_usb_device_manager_disconnect_device_finish(manager, res, &err); if (err) { SPICE_DEBUG("Device disconnection failed"); g_error_free(err); } connect_cb_data_free(data); } static void checkbox_clicked_cb(GtkWidget *check, gpointer user_data); static void connect_cb(GObject *gobject, GAsyncResult *res, gpointer user_data) { SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(gobject); connect_cb_data *data = user_data; SpiceUsbDeviceWidget *self = data->self; SpiceUsbDeviceWidgetPrivate *priv = self->priv; SpiceUsbDevice *device; GError *err = NULL; gchar *desc; spice_usb_device_manager_connect_device_finish(manager, res, &err); if (err) { device = g_object_get_data(G_OBJECT(data->check), "usb-device"); desc = spice_usb_device_get_description(device, priv->device_format_string); g_prefix_error(&err, "Could not redirect %s: ", desc); g_free(desc); SPICE_DEBUG("%s", err->message); g_signal_emit(self, signals[CONNECT_FAILED], 0, device, err); g_error_free(err); /* don't trigger a disconnect if connect failed */ g_signal_handlers_block_by_func(GTK_TOGGLE_BUTTON(data->check), checkbox_clicked_cb, self); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->check), FALSE); g_signal_handlers_unblock_by_func(GTK_TOGGLE_BUTTON(data->check), checkbox_clicked_cb, self); } connect_cb_data_free(data); } static void checkbox_clicked_cb(GtkWidget *check, gpointer user_data) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data); SpiceUsbDeviceWidgetPrivate *priv = self->priv; SpiceUsbDevice *device; device = g_object_get_data(G_OBJECT(check), "usb-device"); connect_cb_data *data = g_new(connect_cb_data, 1); data->check = g_object_ref(check); data->self = g_object_ref(self); if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check))) { spice_usb_device_manager_connect_device_async(priv->manager, device, NULL, connect_cb, data); } else { spice_usb_device_manager_disconnect_device_async(priv->manager, device, NULL, _disconnect_cb, data); } spice_usb_device_widget_update_status(self); } static void checkbox_usb_device_destroy_notify(gpointer data) { g_boxed_free(spice_usb_device_get_type(), data); } static void device_added_cb(SpiceUsbDeviceManager *manager, SpiceUsbDevice *device, gpointer user_data) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data); SpiceUsbDeviceWidgetPrivate *priv = self->priv; GtkWidget *check; gchar *desc; desc = spice_usb_device_get_description(device, priv->device_format_string); check = gtk_check_button_new_with_label(desc); g_free(desc); if (spice_usb_device_manager_is_device_connected(priv->manager, device)) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), TRUE); g_object_set_data_full(G_OBJECT(check), "usb-device", g_boxed_copy(spice_usb_device_get_type(), device), checkbox_usb_device_destroy_notify); g_signal_connect(G_OBJECT(check), "clicked", G_CALLBACK(checkbox_clicked_cb), self); gtk_widget_set_margin_start(check, 12); gtk_box_pack_end(GTK_BOX(self), check, FALSE, FALSE, 0); gtk_box_reorder_child(GTK_BOX(self), priv->empty_cd, -1); if (spice_usb_device_manager_is_device_shared_cd(priv->manager, device) && !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check))) { /* checkbox toggl will initiate redirect */ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), TRUE); } spice_usb_device_widget_update_status(self); gtk_widget_show_all(check); } static void destroy_widget_by_usb_device(GtkWidget *widget, gpointer user_data) { if (get_usb_device(widget) == user_data) { gtk_widget_destroy(widget); } } static void device_removed_cb(SpiceUsbDeviceManager *manager, SpiceUsbDevice *device, gpointer user_data) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data); gtk_container_foreach(GTK_CONTAINER(self), destroy_widget_by_usb_device, device); spice_usb_device_widget_update_status(self); } static void set_inactive_by_usb_device(GtkWidget *check, gpointer user_data) { if (get_usb_device(check) == user_data) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), FALSE); } } static void device_error_cb(SpiceUsbDeviceManager *manager, SpiceUsbDevice *device, GError *err, gpointer user_data) { SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data); gtk_container_foreach(GTK_CONTAINER(self), set_inactive_by_usb_device, device); spice_usb_device_widget_update_status(self); }