1 /* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2 /*
3    Copyright (C) 2012 Red Hat, Inc.
4 
5    Red Hat Authors:
6    Hans de Goede <hdegoede@redhat.com>
7 
8    This library is free software; you can redistribute it and/or
9    modify it under the terms of the GNU Lesser General Public
10    License as published by the Free Software Foundation; either
11    version 2.1 of the License, or (at your option) any later version.
12 
13    This library is distributed in the hope that it will be useful,
14    but WITHOUT ANY WARRANTY; without even the implied warranty of
15    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16    Lesser General Public License for more details.
17 
18    You should have received a copy of the GNU Lesser General Public
19    License along with this library; if not, see <http://www.gnu.org/licenses/>.
20 */
21 
22 #include "config.h"
23 #include <glib/gi18n-lib.h>
24 #include "spice-client.h"
25 #include "spice-marshal.h"
26 #include "usb-device-widget.h"
27 
28 /**
29  * SECTION:usb-device-widget
30  * @short_description: USB device selection widget
31  * @title: Spice USB device selection widget
32  * @section_id:
33  * @see_also:
34  * @stability: Stable
35  * @include: spice-client-gtk.h
36  *
37  * #SpiceUsbDeviceWidget is a gtk widget which apps can use to easily
38  * add an UI to select USB devices to redirect (or unredirect).
39  */
40 
41 struct _SpiceUsbDeviceWidget
42 {
43     GtkBox parent;
44 
45     SpiceUsbDeviceWidgetPrivate *priv;
46 };
47 
48 struct _SpiceUsbDeviceWidgetClass
49 {
50     GtkBoxClass parent_class;
51 
52     /* signals */
53     void (*connect_failed) (SpiceUsbDeviceWidget *widget,
54                             SpiceUsbDevice *device, GError *error);
55 };
56 
57 /* ------------------------------------------------------------------ */
58 /* Prototypes for callbacks  */
59 static void device_added_cb(SpiceUsbDeviceManager *manager,
60     SpiceUsbDevice *device, gpointer user_data);
61 static void device_removed_cb(SpiceUsbDeviceManager *manager,
62     SpiceUsbDevice *device, gpointer user_data);
63 static void device_error_cb(SpiceUsbDeviceManager *manager,
64     SpiceUsbDevice *device, GError *err, gpointer user_data);
65 static gboolean spice_usb_device_widget_update_status(gpointer user_data);
66 
67 enum {
68     PROP_0,
69     PROP_SESSION,
70     PROP_DEVICE_FORMAT_STRING,
71 };
72 
73 enum {
74     CONNECT_FAILED,
75     LAST_SIGNAL,
76 };
77 
78 struct _SpiceUsbDeviceWidgetPrivate {
79     SpiceSession *session;
80     gchar *device_format_string;
81     SpiceUsbDeviceManager *manager;
82     GtkWidget *info_bar;
83     GtkWidget *label;
84     gchar *err_msg;
85     gsize device_count;
86 };
87 
88 static guint signals[LAST_SIGNAL] = { 0, };
89 
G_DEFINE_TYPE_WITH_PRIVATE(SpiceUsbDeviceWidget,spice_usb_device_widget,GTK_TYPE_BOX)90 G_DEFINE_TYPE_WITH_PRIVATE(SpiceUsbDeviceWidget, spice_usb_device_widget, GTK_TYPE_BOX)
91 
92 static void spice_usb_device_widget_get_property(GObject     *gobject,
93                                                  guint        prop_id,
94                                                  GValue      *value,
95                                                  GParamSpec  *pspec)
96 {
97     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject);
98     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
99 
100     switch (prop_id) {
101     case PROP_SESSION:
102         g_value_set_object(value, priv->session);
103         break;
104     case PROP_DEVICE_FORMAT_STRING:
105         g_value_set_string(value, priv->device_format_string);
106         break;
107     default:
108         G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
109         break;
110     }
111 }
112 
spice_usb_device_widget_set_property(GObject * gobject,guint prop_id,const GValue * value,GParamSpec * pspec)113 static void spice_usb_device_widget_set_property(GObject       *gobject,
114                                                  guint          prop_id,
115                                                  const GValue  *value,
116                                                  GParamSpec    *pspec)
117 {
118     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject);
119     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
120 
121     switch (prop_id) {
122     case PROP_SESSION:
123         priv->session = g_value_dup_object(value);
124         break;
125     case PROP_DEVICE_FORMAT_STRING:
126         priv->device_format_string = g_value_dup_string(value);
127         break;
128     default:
129         G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
130         break;
131     }
132 }
133 
spice_usb_device_widget_hide_info_bar(SpiceUsbDeviceWidget * self)134 static void spice_usb_device_widget_hide_info_bar(SpiceUsbDeviceWidget *self)
135 {
136     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
137 
138     g_clear_pointer(&priv->info_bar, gtk_widget_destroy);
139 }
140 
141 static void
spice_usb_device_widget_show_info_bar(SpiceUsbDeviceWidget * self,const gchar * message,GtkMessageType message_type,const gchar * stock_icon_id)142 spice_usb_device_widget_show_info_bar(SpiceUsbDeviceWidget *self,
143                                       const gchar          *message,
144                                       GtkMessageType        message_type,
145                                       const gchar          *stock_icon_id)
146 {
147     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
148     GtkWidget *info_bar, *content_area, *hbox, *widget;
149 
150     spice_usb_device_widget_hide_info_bar(self);
151 
152     info_bar = gtk_info_bar_new();
153     gtk_info_bar_set_message_type(GTK_INFO_BAR(info_bar), message_type);
154 
155     content_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_bar));
156     hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
157     gtk_container_add(GTK_CONTAINER(content_area), hbox);
158 
159     widget = gtk_image_new_from_icon_name(stock_icon_id,
160                                           GTK_ICON_SIZE_SMALL_TOOLBAR);
161     gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0);
162 
163     widget = gtk_label_new(message);
164     gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
165 
166     priv->info_bar = info_bar;
167     gtk_widget_set_margin_start(info_bar, 12);
168     gtk_widget_set_halign(info_bar, GTK_ALIGN_FILL);
169     gtk_box_pack_start(GTK_BOX(self), priv->info_bar, FALSE, FALSE, 0);
170     gtk_widget_show_all(priv->info_bar);
171 }
172 
spice_usb_device_widget_constructed(GObject * gobject)173 static void spice_usb_device_widget_constructed(GObject *gobject)
174 {
175     SpiceUsbDeviceWidget *self;
176     SpiceUsbDeviceWidgetPrivate *priv;
177     GPtrArray *devices = NULL;
178     GError *err = NULL;
179     gchar *str;
180 
181     self = SPICE_USB_DEVICE_WIDGET(gobject);
182     priv = self->priv;
183     if (!priv->session)
184         g_error("SpiceUsbDeviceWidget constructed without a session");
185 
186     priv->label = gtk_label_new(NULL);
187     str = g_strdup_printf("<b>%s</b>", _("Select USB devices to redirect"));
188     gtk_label_set_markup(GTK_LABEL (priv->label), str);
189     g_free(str);
190     gtk_label_set_xalign(GTK_LABEL(priv->label), 0.0);
191     gtk_label_set_yalign(GTK_LABEL(priv->label), 0.5);
192     gtk_box_pack_start(GTK_BOX(self), priv->label, FALSE, FALSE, 0);
193 
194     priv->manager = spice_usb_device_manager_get(priv->session, &err);
195     if (err) {
196         spice_usb_device_widget_show_info_bar(self, err->message,
197                                               GTK_MESSAGE_WARNING,
198                                               "dialog-warning");
199         g_clear_error(&err);
200         return;
201     }
202 
203     g_signal_connect(priv->manager, "device-added",
204                      G_CALLBACK(device_added_cb), self);
205     g_signal_connect(priv->manager, "device-removed",
206                      G_CALLBACK(device_removed_cb), self);
207     g_signal_connect(priv->manager, "device-error",
208                      G_CALLBACK(device_error_cb), self);
209 
210     devices = spice_usb_device_manager_get_devices(priv->manager);
211     if (devices != NULL) {
212         int i;
213         for (i = 0; i < devices->len; i++) {
214             device_added_cb(NULL, g_ptr_array_index(devices, i), self);
215         }
216 
217         g_ptr_array_unref(devices);
218     }
219 
220     spice_usb_device_widget_update_status(self);
221 }
222 
spice_usb_device_widget_finalize(GObject * object)223 static void spice_usb_device_widget_finalize(GObject *object)
224 {
225     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(object);
226     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
227 
228     if (priv->manager) {
229         g_signal_handlers_disconnect_by_func(priv->manager,
230                                              device_added_cb, self);
231         g_signal_handlers_disconnect_by_func(priv->manager,
232                                              device_removed_cb, self);
233         g_signal_handlers_disconnect_by_func(priv->manager,
234                                              device_error_cb, self);
235     }
236     g_object_unref(priv->session);
237     g_free(priv->device_format_string);
238 
239     if (G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize)
240         G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize(object);
241 }
242 
spice_usb_device_widget_class_init(SpiceUsbDeviceWidgetClass * klass)243 static void spice_usb_device_widget_class_init(
244     SpiceUsbDeviceWidgetClass *klass)
245 {
246     GObjectClass *gobject_class = (GObjectClass *)klass;
247     GParamSpec *pspec;
248 
249     gobject_class->constructed  = spice_usb_device_widget_constructed;
250     gobject_class->finalize     = spice_usb_device_widget_finalize;
251     gobject_class->get_property = spice_usb_device_widget_get_property;
252     gobject_class->set_property = spice_usb_device_widget_set_property;
253 
254     /**
255      * SpiceUsbDeviceWidget:session:
256      *
257      * #SpiceSession this #SpiceUsbDeviceWidget is associated with
258      *
259      **/
260     pspec = g_param_spec_object("session",
261                                 "Session",
262                                 "SpiceSession",
263                                 SPICE_TYPE_SESSION,
264                                 G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
265                                 G_PARAM_STATIC_STRINGS);
266     g_object_class_install_property(gobject_class, PROP_SESSION, pspec);
267 
268     /**
269      * SpiceUsbDeviceWidget:device-format-string:
270      *
271      * Format string to pass to spice_usb_device_get_description() for getting
272      * the device USB descriptions.
273      */
274     pspec = g_param_spec_string("device-format-string",
275                                 "Device format string",
276                                 "Format string for device description",
277                                 NULL,
278                                 G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
279                                 G_PARAM_STATIC_STRINGS);
280     g_object_class_install_property(gobject_class, PROP_DEVICE_FORMAT_STRING,
281                                     pspec);
282 
283     /**
284      * SpiceUsbDeviceWidget::connect-failed:
285      * @widget: The #SpiceUsbDeviceWidget that emitted the signal
286      * @device: #SpiceUsbDevice boxed object corresponding to the added device
287      * @error:  #GError describing the reason why the connect failed
288      *
289      * The #SpiceUsbDeviceWidget::connect-failed signal is emitted whenever
290      * the user has requested for a device to be redirected and this has
291      * failed.
292      **/
293     signals[CONNECT_FAILED] =
294         g_signal_new("connect-failed",
295                     G_OBJECT_CLASS_TYPE(gobject_class),
296                     G_SIGNAL_RUN_FIRST,
297                     G_STRUCT_OFFSET(SpiceUsbDeviceWidgetClass, connect_failed),
298                     NULL, NULL,
299                     g_cclosure_user_marshal_VOID__BOXED_BOXED,
300                     G_TYPE_NONE,
301                     2,
302                     SPICE_TYPE_USB_DEVICE,
303                     G_TYPE_ERROR);
304 }
305 
spice_usb_device_widget_init(SpiceUsbDeviceWidget * self)306 static void spice_usb_device_widget_init(SpiceUsbDeviceWidget *self)
307 {
308     self->priv = spice_usb_device_widget_get_instance_private(self);
309 }
310 
311 /* ------------------------------------------------------------------ */
312 /* public api                                                         */
313 
314 /**
315  * spice_usb_device_widget_new:
316  * @session: #SpiceSession for which to widget will control USB redirection
317  * @device_format_string: (allow-none): String passed to
318  * spice_usb_device_get_description()
319  *
320  * Creates a new widget to control USB redirection.
321  *
322  * Returns: a new #SpiceUsbDeviceWidget instance
323  */
spice_usb_device_widget_new(SpiceSession * session,const gchar * device_format_string)324 GtkWidget *spice_usb_device_widget_new(SpiceSession    *session,
325                                        const gchar     *device_format_string)
326 {
327     return g_object_new(SPICE_TYPE_USB_DEVICE_WIDGET,
328                         "orientation", GTK_ORIENTATION_VERTICAL,
329                         "session", session,
330                         "device-format-string", device_format_string,
331                         "spacing", 6,
332                         NULL);
333 }
334 
335 /* ------------------------------------------------------------------ */
336 /* callbacks                                                          */
337 
get_usb_device(GtkWidget * widget)338 static SpiceUsbDevice *get_usb_device(GtkWidget *widget)
339 {
340     return g_object_get_data(G_OBJECT(widget), "usb-device");
341 }
342 
check_can_redirect(GtkWidget * widget,gpointer user_data)343 static void check_can_redirect(GtkWidget *widget, gpointer user_data)
344 {
345     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
346     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
347     SpiceUsbDevice *device;
348     gboolean can_redirect;
349     GError *err = NULL;
350 
351     device = get_usb_device(widget);
352     if (!device)
353         return; /* Non device widget, ie the info_bar */
354 
355     priv->device_count++;
356 
357     if (spice_usb_device_manager_is_redirecting(priv->manager)) {
358         can_redirect = FALSE;
359     } else {
360         can_redirect = spice_usb_device_manager_can_redirect_device(priv->manager,
361                                                                     device, &err);
362         /* If we cannot redirect this device, append the error message to
363            err_msg, but only if it is *not* already there! */
364         if (!can_redirect) {
365             if (priv->err_msg) {
366                 if (!strstr(priv->err_msg, err->message)) {
367                     gchar *old_err_msg = priv->err_msg;
368                     priv->err_msg = g_strdup_printf("%s\n%s", priv->err_msg,
369                                                     err->message);
370                     g_free(old_err_msg);
371                 }
372             } else {
373                 priv->err_msg = g_strdup(err->message);
374             }
375         }
376         g_clear_error(&err);
377     }
378     gtk_widget_set_sensitive(widget, can_redirect);
379 }
380 
spice_usb_device_widget_update_status(gpointer user_data)381 static gboolean spice_usb_device_widget_update_status(gpointer user_data)
382 {
383     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
384     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
385     gchar *str, *markup_str;
386     const gchar *free_channels_str;
387     int free_channels;
388     gboolean redirecting;
389 
390     redirecting = spice_usb_device_manager_is_redirecting(priv->manager);
391 
392     g_object_get(priv->manager, "free-channels", &free_channels, NULL);
393     free_channels_str = g_dngettext(GETTEXT_PACKAGE,
394                                     "Select USB devices to redirect (%d free channel)",
395                                     "Select USB devices to redirect (%d free channels)",
396                                     free_channels);
397     str = g_strdup_printf(free_channels_str, free_channels);
398     markup_str = g_strdup_printf("<b>%s</b>", str);
399     gtk_label_set_markup(GTK_LABEL (priv->label), markup_str);
400     g_free(markup_str);
401     g_free(str);
402 
403     priv->device_count = 0;
404     gtk_container_foreach(GTK_CONTAINER(self), check_can_redirect, self);
405 
406     if (priv->err_msg) {
407         spice_usb_device_widget_show_info_bar(self, priv->err_msg,
408                                               GTK_MESSAGE_INFO,
409                                               "dialog-warning");
410         g_clear_pointer(&priv->err_msg, g_free);
411     } else if (redirecting) {
412         spice_usb_device_widget_show_info_bar(self, _("Redirecting USB Device..."),
413                                               GTK_MESSAGE_INFO,
414                                               "dialog-information");
415     } else {
416         spice_usb_device_widget_hide_info_bar(self);
417     }
418 
419     if (priv->device_count == 0)
420         spice_usb_device_widget_show_info_bar(self, _("No USB devices detected"),
421                                               GTK_MESSAGE_INFO,
422                                               "dialog-information");
423     return FALSE;
424 }
425 
426 typedef struct _connect_cb_data {
427     GtkWidget *check;
428     SpiceUsbDeviceWidget *self;
429 } connect_cb_data;
430 
connect_cb_data_free(connect_cb_data * data)431 static void connect_cb_data_free(connect_cb_data *data)
432 {
433     spice_usb_device_widget_update_status(data->self);
434     g_object_unref(data->check);
435     g_object_unref(data->self);
436     g_free(data);
437 }
438 
_disconnect_cb(GObject * gobject,GAsyncResult * res,gpointer user_data)439 static void _disconnect_cb(GObject *gobject, GAsyncResult *res, gpointer user_data)
440 {
441     SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(gobject);
442     connect_cb_data *data = user_data;
443     GError *err = NULL;
444 
445     spice_usb_device_manager_disconnect_device_finish(manager, res, &err);
446     if (err) {
447         SPICE_DEBUG("Device disconnection failed");
448         g_error_free(err);
449     }
450 
451     connect_cb_data_free(data);
452 }
453 
454 static void checkbox_clicked_cb(GtkWidget *check, gpointer user_data);
connect_cb(GObject * gobject,GAsyncResult * res,gpointer user_data)455 static void connect_cb(GObject *gobject, GAsyncResult *res, gpointer user_data)
456 {
457     SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(gobject);
458     connect_cb_data *data = user_data;
459     SpiceUsbDeviceWidget *self = data->self;
460     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
461     SpiceUsbDevice *device;
462     GError *err = NULL;
463     gchar *desc;
464 
465     spice_usb_device_manager_connect_device_finish(manager, res, &err);
466     if (err) {
467         device = g_object_get_data(G_OBJECT(data->check), "usb-device");
468         desc = spice_usb_device_get_description(device,
469                                                 priv->device_format_string);
470         g_prefix_error(&err, "Could not redirect %s: ", desc);
471         g_free(desc);
472 
473         SPICE_DEBUG("%s", err->message);
474         g_signal_emit(self, signals[CONNECT_FAILED], 0, device, err);
475         g_error_free(err);
476 
477         /* don't trigger a disconnect if connect failed */
478         g_signal_handlers_block_by_func(GTK_TOGGLE_BUTTON(data->check),
479                                         checkbox_clicked_cb, self);
480         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->check), FALSE);
481         g_signal_handlers_unblock_by_func(GTK_TOGGLE_BUTTON(data->check),
482                                         checkbox_clicked_cb, self);
483     }
484 
485     connect_cb_data_free(data);
486 }
487 
checkbox_clicked_cb(GtkWidget * check,gpointer user_data)488 static void checkbox_clicked_cb(GtkWidget *check, gpointer user_data)
489 {
490     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
491     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
492     SpiceUsbDevice *device;
493 
494     device = g_object_get_data(G_OBJECT(check), "usb-device");
495     connect_cb_data *data = g_new(connect_cb_data, 1);
496     data->check = g_object_ref(check);
497     data->self  = g_object_ref(self);
498 
499     if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check))) {
500         spice_usb_device_manager_connect_device_async(priv->manager,
501                                                       device,
502                                                       NULL,
503                                                       connect_cb,
504                                                       data);
505     } else {
506         spice_usb_device_manager_disconnect_device_async(priv->manager,
507                                                          device,
508                                                          NULL,
509                                                          _disconnect_cb,
510                                                          data);
511 
512     }
513     spice_usb_device_widget_update_status(self);
514 }
515 
checkbox_usb_device_destroy_notify(gpointer data)516 static void checkbox_usb_device_destroy_notify(gpointer data)
517 {
518     g_boxed_free(spice_usb_device_get_type(), data);
519 }
520 
device_added_cb(SpiceUsbDeviceManager * manager,SpiceUsbDevice * device,gpointer user_data)521 static void device_added_cb(SpiceUsbDeviceManager *manager,
522     SpiceUsbDevice *device, gpointer user_data)
523 {
524     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
525     SpiceUsbDeviceWidgetPrivate *priv = self->priv;
526     GtkWidget *check;
527     gchar *desc;
528 
529     desc = spice_usb_device_get_description(device,
530                                             priv->device_format_string);
531     check = gtk_check_button_new_with_label(desc);
532     g_free(desc);
533 
534     if (spice_usb_device_manager_is_device_connected(priv->manager,
535                                                      device))
536         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), TRUE);
537 
538     g_object_set_data_full(
539             G_OBJECT(check), "usb-device",
540             g_boxed_copy(spice_usb_device_get_type(), device),
541             checkbox_usb_device_destroy_notify);
542     g_signal_connect(G_OBJECT(check), "clicked",
543                      G_CALLBACK(checkbox_clicked_cb), self);
544 
545     gtk_widget_set_margin_start(check, 12);
546     gtk_box_pack_end(GTK_BOX(self), check, FALSE, FALSE, 0);
547     spice_usb_device_widget_update_status(self);
548     gtk_widget_show_all(check);
549 }
550 
destroy_widget_by_usb_device(GtkWidget * widget,gpointer user_data)551 static void destroy_widget_by_usb_device(GtkWidget *widget, gpointer user_data)
552 {
553     if (get_usb_device(widget) == user_data)
554         gtk_widget_destroy(widget);
555 }
556 
device_removed_cb(SpiceUsbDeviceManager * manager,SpiceUsbDevice * device,gpointer user_data)557 static void device_removed_cb(SpiceUsbDeviceManager *manager,
558     SpiceUsbDevice *device, gpointer user_data)
559 {
560     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
561 
562     gtk_container_foreach(GTK_CONTAINER(self),
563                           destroy_widget_by_usb_device, device);
564 
565     spice_usb_device_widget_update_status(self);
566 }
567 
set_inactive_by_usb_device(GtkWidget * widget,gpointer user_data)568 static void set_inactive_by_usb_device(GtkWidget *widget, gpointer user_data)
569 {
570     if (get_usb_device(widget) == user_data) {
571         GtkWidget *check = gtk_bin_get_child(GTK_BIN(widget));
572         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), FALSE);
573     }
574 }
575 
device_error_cb(SpiceUsbDeviceManager * manager,SpiceUsbDevice * device,GError * err,gpointer user_data)576 static void device_error_cb(SpiceUsbDeviceManager *manager,
577     SpiceUsbDevice *device, GError *err, gpointer user_data)
578 {
579     SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
580 
581     gtk_container_foreach(GTK_CONTAINER(self),
582                           set_inactive_by_usb_device, device);
583 
584     spice_usb_device_widget_update_status(self);
585 }
586