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