more clipboard bits
[spice-gtk:spice-gtk-elmarco.git] / gtk / spice-widget.c
1 #include "spice-widget.h"
2 #include "spice-common.h"
3
4 #include "vncdisplaykeymap.h"
5
6 #include <sys/ipc.h>
7 #include <sys/shm.h>
8
9 #include <X11/Xlib.h>
10 #include <X11/extensions/XShm.h>
11
12 #include <gdk/gdkx.h>
13
14 #include <spice/vd_agent.h>
15
16 #define SPICE_DISPLAY_GET_PRIVATE(obj)                                  \
17     (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_DISPLAY, spice_display))
18
19 struct spice_display {
20     gint                    channel_id;
21
22     /* options */
23     bool                    keyboard_grab_enable;
24     bool                    mouse_grab_enable;
25     bool                    resize_guest_enable;
26     bool                    auto_clipboard_enable;
27
28     /* state */
29     enum SpiceSurfaceFmt    format;
30     gint                    width, height, stride;
31     gint                    shmid;
32     gpointer                data;
33
34     gint                    ww, wh, mx, my;
35
36     bool                    convert;
37     bool                    have_mitshm;
38     Display                 *dpy;
39     XVisualInfo             *vi;
40     XImage                  *ximage;
41     XShmSegmentInfo         *shminfo;
42     GC                      gc;
43
44     GtkClipboard            *clipboard;
45     bool                    clip_hasdata;
46     bool                    clip_grabbed;
47
48     SpiceSession            *session;
49     SpiceChannel            *main;
50     SpiceChannel            *display;
51     SpiceCursorChannel      *cursor;
52     SpiceInputsChannel      *inputs;
53
54     enum SpiceMouseMode     mouse_mode;
55     int                     mouse_grab_active;
56     bool                    mouse_have_pointer;
57     GdkCursor               *mouse_cursor;
58     int                     mouse_last_x;
59     int                     mouse_last_y;
60
61     bool                    keyboard_grab_active;
62     bool                    keyboard_have_focus;
63     int                     keyboard_grab_count;
64     time_t                  keyboard_grab_time;
65
66     const guint16 const     *keycode_map;
67     size_t                  keycode_maplen;
68
69     gint                    timer_id;
70 };
71
72 G_DEFINE_TYPE(SpiceDisplay, spice_display, GTK_TYPE_DRAWING_AREA)
73
74 /* Properties */
75 enum {
76     PROP_0,
77     PROP_KEYBOARD_GRAB,
78     PROP_MOUSE_GRAB,
79     PROP_RESIZE_GUEST,
80     PROP_AUTO_CLIPBOARD,
81 };
82
83 #if 0
84 /* Signals */
85 enum {
86     SPICE_DISPLAY_FOO,
87     SPICE_DISPLAY_LAST_SIGNAL,
88 };
89
90 static guint signals[SPICE_DISPLAY_LAST_SIGNAL];
91 #endif
92
93 static bool no_mitshm;
94
95 static void try_keyboard_grab(GtkWidget *widget);
96 static void try_keyboard_ungrab(GtkWidget *widget);
97 static void try_mouse_grab(GtkWidget *widget);
98 static void try_mouse_ungrab(GtkWidget *widget);
99 static void recalc_geometry(GtkWidget *widget);
100 static void clipboard_owner_change(GtkClipboard *clipboard,
101                                    GdkEventOwnerChange *event, gpointer user_data);
102
103 /* ---------------------------------------------------------------- */
104
105 static struct format_table {
106     enum SpiceSurfaceFmt  spice;
107     XVisualInfo           xvisual;
108 } format_table[] = {
109     {
110         .spice = SPICE_SURFACE_FMT_32_xRGB,
111         .xvisual = {
112             .depth      = 24,
113             .red_mask   = 0xff0000,
114             .green_mask = 0x00ff00,
115             .blue_mask  = 0x0000ff,
116         },
117     },{
118         .spice = SPICE_SURFACE_FMT_16_555,
119         .xvisual = {
120             .depth      = 16,
121             .red_mask   = 0x7c00,
122             .green_mask = 0x03e0,
123             .blue_mask  = 0x001f,
124         },
125     },{
126         .spice = SPICE_SURFACE_FMT_16_565,
127         .xvisual = {
128             .depth      = 16,
129             .red_mask   = 0xf800,
130             .green_mask = 0x07e0,
131             .blue_mask  = 0x001f,
132         },
133     }
134 };
135
136 /* ---------------------------------------------------------------- */
137
138 static void spice_display_get_property(GObject    *object,
139                                        guint       prop_id,
140                                        GValue     *value,
141                                        GParamSpec *pspec)
142 {
143     SpiceDisplay *display = SPICE_DISPLAY(object);
144     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
145
146     switch (prop_id) {
147     case PROP_KEYBOARD_GRAB:
148         g_value_set_boolean(value, d->keyboard_grab_enable);
149         break;
150     case PROP_MOUSE_GRAB:
151         g_value_set_boolean(value, d->mouse_grab_enable);
152         break;
153     case PROP_RESIZE_GUEST:
154         g_value_set_boolean(value, d->resize_guest_enable);
155         break;
156     case PROP_AUTO_CLIPBOARD:
157         g_value_set_boolean(value, d->auto_clipboard_enable);
158         break;
159     default:
160         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
161         break;
162     }
163 }
164
165 static void spice_display_set_property(GObject      *object,
166                                        guint         prop_id,
167                                        const GValue *value,
168                                        GParamSpec   *pspec)
169 {
170     SpiceDisplay *display = SPICE_DISPLAY(object);
171     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
172
173     switch (prop_id) {
174     case PROP_KEYBOARD_GRAB:
175         d->keyboard_grab_enable = g_value_get_boolean(value);
176         if (d->keyboard_grab_enable) {
177             try_keyboard_grab(GTK_WIDGET(display));
178         } else {
179             try_keyboard_ungrab(GTK_WIDGET(display));
180         }
181         break;
182     case PROP_MOUSE_GRAB:
183         d->mouse_grab_enable = g_value_get_boolean(value);
184         if (!d->mouse_grab_enable) {
185             try_mouse_ungrab(GTK_WIDGET(display));
186         }
187         break;
188     case PROP_RESIZE_GUEST:
189         d->resize_guest_enable = g_value_get_boolean(value);
190         if (d->resize_guest_enable) {
191             gtk_widget_set_size_request(GTK_WIDGET(display), 640, 480);
192             recalc_geometry(GTK_WIDGET(display));
193         } else {
194             gtk_widget_set_size_request(GTK_WIDGET(display),
195                                         d->width, d->height);
196         }
197         break;
198     case PROP_AUTO_CLIPBOARD:
199         d->auto_clipboard_enable = g_value_get_boolean(value);
200         break;
201     default:
202         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
203         break;
204     }
205 }
206
207 static void spice_display_destroy(GtkObject *obj)
208 {
209     GTK_OBJECT_CLASS(spice_display_parent_class)->destroy(obj);
210 }
211
212 static void spice_display_finalize(GObject *obj)
213 {
214     G_OBJECT_CLASS(spice_display_parent_class)->finalize(obj);
215 }
216
217 static void spice_display_init(SpiceDisplay *display)
218 {
219     GtkWidget *widget = GTK_WIDGET(display);
220     spice_display *d;
221
222     d = display->priv = SPICE_DISPLAY_GET_PRIVATE(display);
223     memset(d, 0, sizeof(*d));
224
225     gtk_widget_add_events(widget,
226                           GDK_STRUCTURE_MASK |
227                           GDK_POINTER_MOTION_MASK |
228                           GDK_BUTTON_PRESS_MASK |
229                           GDK_BUTTON_RELEASE_MASK |
230                           GDK_BUTTON_MOTION_MASK |
231                           GDK_ENTER_NOTIFY_MASK |
232                           GDK_LEAVE_NOTIFY_MASK |
233                           GDK_KEY_PRESS_MASK);
234     gtk_widget_set_double_buffered(widget, false);
235     gtk_widget_set_can_focus(widget, true);
236
237     d->keycode_map = vnc_display_keymap_gdk2xtkbd_table(&d->keycode_maplen);
238
239     d->clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
240     g_signal_connect(G_OBJECT(d->clipboard), "owner-change",
241                      G_CALLBACK(clipboard_owner_change), display);
242
243     d->have_mitshm = true;
244 }
245
246 static void try_keyboard_grab(GtkWidget *widget)
247 {
248     SpiceDisplay *display = SPICE_DISPLAY(widget);
249     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
250     time_t now;
251
252     if (d->keyboard_grab_active)
253         return;
254
255     if (!d->keyboard_grab_enable)
256         return;
257     if (!d->keyboard_have_focus)
258         return;
259     if (!d->mouse_have_pointer)
260         return;
261
262 #if 1
263     /*
264      * == DEBUG ==
265      * focus / keyboard grab behavior is funky
266      * when going fullscreen (with KDE):
267      * focus-in-event -> grab -> focus-out-event -> ungrab -> repeat
268      * I have no idea why the grab triggers focus-out :-(
269      */
270     assert(gtk_widget_is_focus(widget));
271     assert(gtk_widget_has_focus(widget));
272
273     now = time(NULL);
274     if (d->keyboard_grab_time != now) {
275         d->keyboard_grab_time = now;
276         d->keyboard_grab_count = 0;
277     }
278     if (d->keyboard_grab_count++ > 32) {
279         fprintf(stderr, "%s: 32 grabs last second -> emergency exit\n",
280                 __FUNCTION__);
281         return;
282     }
283 #endif
284
285 #if 0
286     fprintf(stderr, "grab keyboard\n");
287 #endif
288
289     gdk_keyboard_grab(gtk_widget_get_window(widget), FALSE,
290                       GDK_CURRENT_TIME);
291     d->keyboard_grab_active = true;
292 }
293
294
295 static void try_keyboard_ungrab(GtkWidget *widget)
296 {
297     SpiceDisplay *display = SPICE_DISPLAY(widget);
298     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
299
300     if (!d->keyboard_grab_active)
301         return;
302
303 #if 0
304     fprintf(stderr, "ungrab keyboard\n");
305 #endif
306     gdk_keyboard_ungrab(GDK_CURRENT_TIME);
307     d->keyboard_grab_active = false;
308 }
309
310 static void try_mouse_grab(GtkWidget *widget)
311 {
312     SpiceDisplay *display = SPICE_DISPLAY(widget);
313     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
314
315     if (!d->mouse_grab_enable)
316         return;
317     if (d->mouse_mode != SPICE_MOUSE_MODE_SERVER)
318         return;
319     if (d->mouse_grab_active)
320         return;
321
322     gdk_pointer_grab(gtk_widget_get_window(widget),
323                      FALSE, /* All events to come to our window directly */
324                      GDK_POINTER_MOTION_MASK |
325                      GDK_BUTTON_PRESS_MASK |
326                      GDK_BUTTON_RELEASE_MASK |
327                      GDK_BUTTON_MOTION_MASK,
328                      NULL, /* Allow cursor to move over entire desktop */
329                      gdk_cursor_new(GDK_BLANK_CURSOR),
330                      GDK_CURRENT_TIME);
331     d->mouse_grab_active = true;
332     d->mouse_last_x = -1;
333     d->mouse_last_y = -1;
334 }
335
336 static void mouse_check_edges(GtkWidget *widget, GdkEventMotion *motion)
337 {
338     SpiceDisplay *display = SPICE_DISPLAY(widget);
339     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
340     GdkDrawable *drawable = GDK_DRAWABLE(gtk_widget_get_window(widget));
341     GdkScreen *screen = gdk_drawable_get_screen(drawable);
342     int x = (int)motion->x_root;
343     int y = (int)motion->y_root;
344
345     /* In relative mode check to see if client pointer hit
346      * one of the screen edges, and if so move it back by
347      * 200 pixels. This is important because the pointer
348      * in the server doesn't correspond 1-for-1, and so
349      * may still be only half way across the screen. Without
350      * this warp, the server pointer would thus appear to hit
351      * an invisible wall */
352     if (x == 0) x += 200;
353     if (y == 0) y += 200;
354     if (x == (gdk_screen_get_width(screen) - 1)) x -= 200;
355     if (y == (gdk_screen_get_height(screen) - 1)) y -= 200;
356
357     if (x != (int)motion->x_root || y != (int)motion->y_root) {
358         gdk_display_warp_pointer(gdk_drawable_get_display(drawable),
359                                  screen, x, y);
360         d->mouse_last_x = -1;
361         d->mouse_last_y = -1;
362     }
363 }
364
365 static void try_mouse_ungrab(GtkWidget *widget)
366 {
367     SpiceDisplay *display = SPICE_DISPLAY(widget);
368     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
369
370     if (!d->mouse_grab_active)
371         return;
372
373     gdk_pointer_ungrab(GDK_CURRENT_TIME);
374     gdk_window_set_cursor(gtk_widget_get_window(widget), NULL);
375     d->mouse_grab_active = false;
376 }
377
378 static gboolean geometry_timer(gpointer data)
379 {
380     SpiceDisplay *display = data;
381     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
382
383     d->timer_id = 0;
384     spice_main_set_display(d->main, d->channel_id,
385                            0, 0, d->ww, d->wh);
386     return false;
387 }
388
389 static void recalc_geometry(GtkWidget *widget)
390 {
391     SpiceDisplay *display = SPICE_DISPLAY(widget);
392     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
393
394     d->mx = 0;
395     d->my = 0;
396     if (d->ww > d->width)
397         d->mx = (d->ww - d->width) / 2;
398     if (d->wh > d->height)
399         d->my = (d->wh - d->height) / 2;
400
401 #if 0
402     fprintf(stderr, "%s: guest %dx%d, window %dx%d, offset +%d+%d\n", __FUNCTION__,
403             d->width, d->height, d->ww, d->wh, d->mx, d->my);
404 #endif
405
406     if (d->timer_id) {
407         g_source_remove(d->timer_id);
408     }
409     if (d->resize_guest_enable) {
410         d->timer_id = g_timeout_add_seconds(1, geometry_timer, display);
411     }
412 }
413
414 static XVisualInfo *get_visual_for_format(GtkWidget *widget, enum SpiceSurfaceFmt format)
415 {
416     GdkDrawable  *drawable = gtk_widget_get_window(widget);
417     GdkDisplay   *display = gdk_drawable_get_display(drawable);
418     GdkScreen    *screen = gdk_drawable_get_screen(drawable);
419     XVisualInfo  template;
420     int          found, i;
421
422     for (i = 0; i < SPICE_N_ELEMENTS(format_table); i++) {
423         if (format == format_table[i].spice)
424             break;
425     }
426     if (i == SPICE_N_ELEMENTS(format_table))
427         return NULL;
428
429     template = format_table[i].xvisual;
430     template.screen = gdk_x11_screen_get_screen_number(screen);
431     return XGetVisualInfo(gdk_x11_display_get_xdisplay(display),
432                           VisualScreenMask | VisualDepthMask |
433                           VisualRedMaskMask | VisualGreenMaskMask | VisualBlueMaskMask,
434                           &template, &found);
435 }
436
437 static XVisualInfo *get_visual_default(GtkWidget *widget)
438 {
439     GdkDrawable  *drawable = gtk_widget_get_window(widget);
440     GdkDisplay   *display = gdk_drawable_get_display(drawable);
441     GdkScreen    *screen = gdk_drawable_get_screen(drawable);
442     XVisualInfo  template;
443     int          found;
444
445     template.screen = gdk_x11_screen_get_screen_number(screen);
446     return XGetVisualInfo(gdk_x11_display_get_xdisplay(display),
447                           VisualScreenMask,
448                           &template, &found);
449 }
450
451 static int catch_no_mitshm(Display * dpy, XErrorEvent * event)
452 {
453     no_mitshm = true;
454     return 0;
455 }
456
457 static int ximage_create(GtkWidget *widget)
458 {
459     SpiceDisplay    *display = SPICE_DISPLAY(widget);
460     spice_display   *d = SPICE_DISPLAY_GET_PRIVATE(display);
461     GdkDrawable     *window = gtk_widget_get_window(widget);
462     GdkDisplay      *gtkdpy = gdk_drawable_get_display(window);
463     void            *old_handler = NULL;
464     XGCValues       gcval = {
465         .foreground = 0,
466         .background = 0,
467     };
468
469     d->dpy = gdk_x11_display_get_xdisplay(gtkdpy);
470     d->convert = false;
471     d->vi = get_visual_for_format(widget, d->format);
472     if (d->vi == NULL) {
473         d->convert = true;
474         d->vi = get_visual_default(widget);
475         ASSERT(d->vi != NULL);
476     }
477     if (d->convert) {
478         PANIC("format conversion not implemented yet");
479     }
480
481     d->gc = XCreateGC(d->dpy, gdk_x11_drawable_get_xid(window),
482                       GCForeground | GCBackground, &gcval);
483
484     if (d->have_mitshm && d->shmid != -1) {
485         if (!XShmQueryExtension(d->dpy)) {
486             goto shm_fail;
487         }
488         no_mitshm = false;
489         old_handler = XSetErrorHandler(catch_no_mitshm);
490         d->shminfo = spice_new0(XShmSegmentInfo, 1);
491         d->ximage = XShmCreateImage(d->dpy, d->vi->visual, d->vi->depth,
492                                     ZPixmap, d->data, d->shminfo, d->width, d->height);
493         if (d->ximage == NULL)
494             goto shm_fail;
495         d->shminfo->shmaddr = d->data;
496         d->shminfo->shmid = d->shmid;
497         d->shminfo->readOnly = false;
498         XShmAttach(d->dpy, d->shminfo);
499         XSync(d->dpy, False);
500         shmctl(d->shmid, IPC_RMID, 0);
501         if (no_mitshm)
502             goto shm_fail;
503         XSetErrorHandler(old_handler);
504         return 0;
505     }
506
507 shm_fail:
508     d->have_mitshm = false;
509     if (old_handler)
510         XSetErrorHandler(old_handler);
511     d->ximage = XCreateImage(d->dpy, d->vi->visual, d->vi->depth, ZPixmap, 0,
512                              d->data, d->width, d->height, 32, d->stride);
513     return 0;
514 }
515
516 static void ximage_destroy(GtkWidget *widget)
517 {
518     SpiceDisplay *display = SPICE_DISPLAY(widget);
519     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
520
521     if (d->ximage) {
522         XDestroyImage(d->ximage);
523         d->ximage = NULL;
524     }
525     if (d->shminfo) {
526         XShmDetach(d->dpy, d->shminfo);
527         free(d->shminfo);
528         d->shminfo = NULL;
529     }
530     if (d->gc) {
531         XFreeGC(d->dpy, d->gc);
532         d->gc = NULL;
533     }
534 }
535
536 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *expose)
537 {
538     SpiceDisplay *display = SPICE_DISPLAY(widget);
539     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
540     GdkDrawable *window = gtk_widget_get_window(widget);
541
542 #if 0
543     fprintf(stderr, "%s: area %dx%d at %d,%d\n", __FUNCTION__,
544             expose->area.width,
545             expose->area.height,
546             expose->area.x,
547             expose->area.y);
548 #endif
549
550     if (d->data == NULL)
551         return true;
552     if (!d->ximage) {
553         ximage_create(widget);
554     }
555
556     if (expose->area.x >= d->mx &&
557         expose->area.y >= d->my &&
558         expose->area.x + expose->area.width  <= d->mx + d->width &&
559         expose->area.y + expose->area.height <= d->my + d->height) {
560         /* area is completely inside the guest screen -- blit it */
561         if (d->have_mitshm) {
562             XShmPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
563                          d->gc, d->ximage,
564                          expose->area.x - d->mx, expose->area.y - d->my,
565                          expose->area.x,         expose->area.y,
566                          expose->area.width, expose->area.height,
567                          true);
568         } else {
569             XPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
570                       d->gc, d->ximage,
571                       expose->area.x - d->mx, expose->area.y - d->my,
572                       expose->area.x,         expose->area.y,
573                       expose->area.width, expose->area.height);
574         }
575     } else {
576         /* complete window update */
577         if (d->ww > d->width || d->wh > d->height) {
578             int x1 = d->mx;
579             int x2 = d->mx + d->width;
580             int y1 = d->my;
581             int y2 = d->my + d->height;
582             XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
583                            d->gc, 0, 0, x1, d->wh);
584             XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
585                            d->gc, x2, 0, d->ww - x2, d->wh);
586             XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
587                            d->gc, 0, 0, d->ww, y1);
588             XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
589                            d->gc, 0, y2, d->ww, d->wh - y2);
590         }
591         if (d->have_mitshm) {
592             XShmPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
593                          d->gc, d->ximage,
594                          0, 0, d->mx, d->my, d->width, d->height,
595                          true);
596         } else {
597             XPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
598                       d->gc, d->ximage,
599                       0, 0, d->mx, d->my, d->width, d->height);
600         }
601     }
602
603     return true;
604 }
605
606 static gboolean key_event(GtkWidget *widget, GdkEventKey *key)
607 {
608     SpiceDisplay *display = SPICE_DISPLAY(widget);
609     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
610     int scancode;
611
612 #if 0
613     fprintf(stderr, "%s %s: keycode: %d  state: %d  group %d\n",
614             __FUNCTION__, key->type == GDK_KEY_PRESS ? "press" : "release",
615             key->hardware_keycode, key->state, key->group);
616 #endif
617
618     if (!d->inputs)
619         return true;
620
621     scancode = vnc_display_keymap_gdk2xtkbd(d->keycode_map, d->keycode_maplen,
622                                             key->hardware_keycode);
623     switch (key->type) {
624     case GDK_KEY_PRESS:
625         spice_inputs_key_press(d->inputs, scancode);
626         break;
627     case GDK_KEY_RELEASE:
628         spice_inputs_key_release(d->inputs, scancode);
629         break;
630     default:
631         break;
632     }
633     return true;
634 }
635
636 static gboolean enter_event(GtkWidget *widget, GdkEventCrossing *crossing G_GNUC_UNUSED)
637 {
638     SpiceDisplay *display = SPICE_DISPLAY(widget);
639     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
640
641 #if 0
642     fprintf(stderr, "%s\n", __FUNCTION__);
643 #endif
644     d->mouse_have_pointer = true;
645     try_keyboard_grab(widget);
646     return true;
647 }
648
649 static gboolean leave_event(GtkWidget *widget, GdkEventCrossing *crossing G_GNUC_UNUSED)
650 {
651     SpiceDisplay *display = SPICE_DISPLAY(widget);
652     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
653
654 #if 0
655     fprintf(stderr, "%s\n", __FUNCTION__);
656 #endif
657     d->mouse_have_pointer = false;
658     try_keyboard_ungrab(widget);
659     return true;
660 }
661
662 static gboolean focus_in_event(GtkWidget *widget, GdkEventFocus *focus G_GNUC_UNUSED)
663 {
664     SpiceDisplay *display = SPICE_DISPLAY(widget);
665     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
666
667 #if 0
668     fprintf(stderr, "%s\n", __FUNCTION__);
669 #endif
670     d->keyboard_have_focus = true;
671     try_keyboard_grab(widget);
672     return true;
673 }
674
675 static gboolean focus_out_event(GtkWidget *widget, GdkEventFocus *focus G_GNUC_UNUSED)
676 {
677     SpiceDisplay *display = SPICE_DISPLAY(widget);
678     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
679
680 #if 0
681     fprintf(stderr, "%s\n", __FUNCTION__);
682 #endif
683     d->keyboard_have_focus = false;
684     try_keyboard_ungrab(widget);
685     return true;
686 }
687
688 static int button_gdk_to_spice(int gdk)
689 {
690     static const int map[] = {
691         [ 1 ] = SPICE_MOUSE_BUTTON_LEFT,
692         [ 2 ] = SPICE_MOUSE_BUTTON_MIDDLE,
693         [ 3 ] = SPICE_MOUSE_BUTTON_RIGHT,
694         [ 4 ] = SPICE_MOUSE_BUTTON_UP,
695         [ 5 ] = SPICE_MOUSE_BUTTON_DOWN,
696     };
697
698     if (gdk < SPICE_N_ELEMENTS(map)) {
699         return map [ gdk ];
700     }
701     return 0;
702 }
703
704 static int button_mask_gdk_to_spice(int gdk)
705 {
706     int spice = 0;
707
708     if (gdk & GDK_BUTTON1_MASK)
709         spice |= SPICE_MOUSE_BUTTON_MASK_LEFT;
710     if (gdk & GDK_BUTTON2_MASK)
711         spice |= SPICE_MOUSE_BUTTON_MASK_MIDDLE;
712     if (gdk & GDK_BUTTON3_MASK)
713         spice |= SPICE_MOUSE_BUTTON_MASK_RIGHT;
714     return spice;
715 }
716
717 static gboolean motion_event(GtkWidget *widget, GdkEventMotion *motion)
718 {
719     SpiceDisplay *display = SPICE_DISPLAY(widget);
720     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
721
722 #if 0
723     fprintf(stderr, "%s: +%.0f+%.0f\n", __FUNCTION__, motion->x, motion->y);
724 #endif
725
726     if (!d->inputs)
727         return true;
728     switch (d->mouse_mode) {
729     case SPICE_MOUSE_MODE_CLIENT:
730         if (motion->x >= d->mx            &&
731             motion->x <  d->mx + d->width &&
732             motion->y >= d->my            &&
733             motion->y <  d->my + d->height) {
734             spice_inputs_position(d->inputs,
735                                   motion->x - d->mx, motion->y - d->my,
736                                   d->channel_id,
737                                   button_mask_gdk_to_spice(motion->state));
738         }
739         break;
740     case SPICE_MOUSE_MODE_SERVER:
741         if (d->mouse_grab_active) {
742             if (d->mouse_last_x != -1 &&
743                 d->mouse_last_y != -1) {
744                 spice_inputs_motion(d->inputs,
745                                     motion->x - d->mouse_last_x,
746                                     motion->y - d->mouse_last_y,
747                                     button_mask_gdk_to_spice(motion->state));
748             }
749             d->mouse_last_x = motion->x;
750             d->mouse_last_y = motion->y;
751             mouse_check_edges(widget, motion);
752         }
753         break;
754     default:
755         break;
756     }
757     return true;
758 }
759
760 static gboolean button_event(GtkWidget *widget, GdkEventButton *button)
761 {
762     SpiceDisplay *display = SPICE_DISPLAY(widget);
763     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
764
765 #if 0
766     fprintf(stderr, "%s %s: button %d\n", __FUNCTION__,
767             button->type == GDK_BUTTON_PRESS ? "press" : "release",
768             button->button);
769 #endif
770
771     gtk_widget_grab_focus(widget);
772     try_mouse_grab(widget);
773
774     if (!d->inputs)
775         return true;
776
777     switch (button->type) {
778     case GDK_BUTTON_PRESS:
779         spice_inputs_button_press(d->inputs,
780                                   button_gdk_to_spice(button->button),
781                                   button_mask_gdk_to_spice(button->state));
782         break;
783     case GDK_BUTTON_RELEASE:
784         spice_inputs_button_release(d->inputs,
785                                     button_gdk_to_spice(button->button),
786                                     button_mask_gdk_to_spice(button->state));
787         break;
788     default:
789         break;
790     }
791     return true;
792 }
793
794 static gboolean configure_event(GtkWidget *widget, GdkEventConfigure *conf)
795 {
796     SpiceDisplay *display = SPICE_DISPLAY(widget);
797     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
798
799     if (conf->width != d->ww  || conf->height != d->wh) {
800         d->ww = conf->width;
801         d->wh = conf->height;
802         recalc_geometry(widget);
803     }
804     return true;
805 }
806
807 /* ---------------------------------------------------------------- */
808
809 static const struct {
810     const char  *x;
811     int         s;
812 } atom2agent[] = {
813     { .s = VD_AGENT_CLIPBOARD_UTF8_TEXT,  .x = "UTF8_STRING"              },
814     { .s = VD_AGENT_CLIPBOARD_UTF8_TEXT,  .x = "text/plain;charset=utf-8" },
815     { .s = VD_AGENT_CLIPBOARD_UTF8_TEXT,  .x = "STRING"                   },
816     { .s = VD_AGENT_CLIPBOARD_UTF8_TEXT,  .x = "TEXT"                     },
817     { .s = VD_AGENT_CLIPBOARD_UTF8_TEXT,  .x = "text/plain"               },
818
819 #if 0 /* gimp */
820     { .s = VD_AGENT_CLIPBOARD_BITMAP,     .x = "image/bmp"                },
821     { .s = VD_AGENT_CLIPBOARD_BITMAP,     .x = "image/x-bmp"              },
822     { .s = VD_AGENT_CLIPBOARD_BITMAP,     .x = "image/x-MS-bmp"           },
823     { .s = VD_AGENT_CLIPBOARD_BITMAP,     .x = "image/x-win-bitmap"       },
824 #endif
825
826 #if 0 /* firefox */
827     { .s = VD_AGENT_CLIPBOARD_HTML,       .x = "text/html"                },
828 #endif
829 };
830
831 static void clipboard_get_targets(GtkClipboard *clipboard,
832                                   GdkAtom *atoms,
833                                   gint n_atoms,
834                                   gpointer data)
835 {
836     SpiceDisplay *display = data;
837     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
838     int types[SPICE_N_ELEMENTS(atom2agent)];
839     char *name;
840     int a, m, t;
841
842 #if 1 /* debug */
843     fprintf(stderr, "%s:", __FUNCTION__);
844     for (a = 0; a < n_atoms; a++) {
845         fprintf(stderr, " %s",gdk_atom_name(atoms[a]));
846     }
847     fprintf(stderr, "\n");
848 #endif
849
850     memset(types, 0, sizeof(types));
851     for (a = 0; a < n_atoms; a++) {
852         name = gdk_atom_name(atoms[a]);
853         for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) {
854             if (strcasecmp(name, atom2agent[m].x) != 0) {
855                 continue;
856             }
857             /* found match */
858             for (t = 0; t < SPICE_N_ELEMENTS(atom2agent); t++) {
859                 if (types[t] == atom2agent[m].s) {
860                     /* type already in list */
861                     break;
862                 }
863                 if (types[t] == 0) {
864                     /* add type to empty slot */
865                     types[t] = atom2agent[m].s;
866                     break;
867                 }
868             }
869             break;
870         }
871     }
872     for (t = 0; t < SPICE_N_ELEMENTS(atom2agent); t++) {
873         if (types[t] == 0) {
874             break;
875         }
876     }
877     if (!d->clip_grabbed && t > 0) {
878         d->clip_grabbed = true;
879         spice_main_clipboard_grab(d->main, types, t);
880     }
881 }
882
883 static void clipboard_owner_change(GtkClipboard        *clipboard,
884                                    GdkEventOwnerChange *event,
885                                    gpointer            data)
886 {
887     SpiceDisplay *display = data;
888     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
889
890     if (d->clip_grabbed) {
891         d->clip_grabbed = false;
892         spice_main_clipboard_release(d->main);
893     }
894
895     switch (event->reason) {
896     case GDK_OWNER_CHANGE_NEW_OWNER:
897         d->clip_hasdata = 1;
898         if (d->auto_clipboard_enable)
899             gtk_clipboard_request_targets(clipboard, clipboard_get_targets, data);
900         break;
901     default:
902         d->clip_hasdata = 0;
903         break;
904     }
905 }
906
907 /* ---------------------------------------------------------------- */
908
909 static void spice_display_class_init(SpiceDisplayClass *klass)
910 {
911     GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
912     GtkObjectClass *gtkobject_class = GTK_OBJECT_CLASS(klass);
913     GtkWidgetClass *gtkwidget_class = GTK_WIDGET_CLASS(klass);
914
915     gtkwidget_class->expose_event = expose_event;
916     gtkwidget_class->key_press_event = key_event;
917     gtkwidget_class->key_release_event = key_event;
918     gtkwidget_class->enter_notify_event = enter_event;
919     gtkwidget_class->leave_notify_event = leave_event;
920     gtkwidget_class->focus_in_event = focus_in_event;
921     gtkwidget_class->focus_out_event = focus_out_event;
922     gtkwidget_class->motion_notify_event = motion_event;
923     gtkwidget_class->button_press_event = button_event;
924     gtkwidget_class->button_release_event = button_event;
925     gtkwidget_class->configure_event = configure_event;
926
927     gtkobject_class->destroy = spice_display_destroy;
928
929     gobject_class->finalize = spice_display_finalize;
930     gobject_class->get_property = spice_display_get_property;
931     gobject_class->set_property = spice_display_set_property;
932
933     g_object_class_install_property
934         (gobject_class, PROP_KEYBOARD_GRAB,
935          g_param_spec_boolean("grab-keyboard",
936                               "Grab Keyboard",
937                               "Whether we should grab the keyboard.",
938                               TRUE,
939                               G_PARAM_READWRITE |
940                               G_PARAM_CONSTRUCT |
941                               G_PARAM_STATIC_NAME |
942                               G_PARAM_STATIC_NICK |
943                               G_PARAM_STATIC_BLURB));
944
945     g_object_class_install_property
946         (gobject_class, PROP_MOUSE_GRAB,
947          g_param_spec_boolean("grab-mouse",
948                               "Grab Mouse",
949                               "Whether we should grab the mouse.",
950                               TRUE,
951                               G_PARAM_READWRITE |
952                               G_PARAM_CONSTRUCT |
953                               G_PARAM_STATIC_NAME |
954                               G_PARAM_STATIC_NICK |
955                               G_PARAM_STATIC_BLURB));
956
957     g_object_class_install_property
958         (gobject_class, PROP_RESIZE_GUEST,
959          g_param_spec_boolean("resize-guest",
960                               "Resize guest",
961                               "Try to adapt guest display on window resize. "
962                               "Requires guest cooperation.",
963                               FALSE,
964                               G_PARAM_READWRITE |
965                               G_PARAM_CONSTRUCT |
966                               G_PARAM_STATIC_NAME |
967                               G_PARAM_STATIC_NICK |
968                               G_PARAM_STATIC_BLURB));
969
970     g_object_class_install_property
971         (gobject_class, PROP_AUTO_CLIPBOARD,
972          g_param_spec_boolean("auto-clipboard",
973                               "Auto clipboard",
974                               "Automatically relay clipboard changes between "
975                               "host and guest.",
976                               FALSE,
977                               G_PARAM_READWRITE |
978                               G_PARAM_CONSTRUCT |
979                               G_PARAM_STATIC_NAME |
980                               G_PARAM_STATIC_NICK |
981                               G_PARAM_STATIC_BLURB));
982
983         g_type_class_add_private(klass, sizeof(spice_display));
984 }
985
986 /* ---------------------------------------------------------------- */
987
988 static void mouse_mode(SpiceChannel *channel, enum SpiceMouseMode mode,
989                        gpointer data)
990 {
991     SpiceDisplay *display = data;
992     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
993
994     d->mouse_mode = mode;
995 }
996
997 static void primary_create(SpiceChannel *channel, gint format,
998                            gint width, gint height, gint stride,
999                            gint shmid, gpointer imgdata, gpointer data)
1000 {
1001     SpiceDisplay *display = data;
1002     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
1003
1004     d->format = format;
1005     d->stride = stride;
1006     d->shmid  = shmid;
1007     d->data   = imgdata;
1008
1009     if (d->width != width || d->height != height) {
1010         d->width  = width;
1011         d->height = height;
1012         recalc_geometry(GTK_WIDGET(display));
1013         if (!d->resize_guest_enable) {
1014             gtk_widget_set_size_request(GTK_WIDGET(display), width, height);
1015         }
1016     }
1017 }
1018
1019 static void primary_destroy(SpiceChannel *channel, gpointer data)
1020 {
1021     SpiceDisplay *display = SPICE_DISPLAY(data);
1022     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
1023
1024     d->format = 0;
1025     d->width  = 0;
1026     d->height = 0;
1027     d->stride = 0;
1028     d->shmid  = 0;
1029     d->data   = 0;
1030     ximage_destroy(GTK_WIDGET(display));
1031 }
1032
1033 static void invalidate(SpiceChannel *channel,
1034                        gint x, gint y, gint w, gint h, gpointer data)
1035 {
1036     SpiceDisplay *display = data;
1037     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
1038     gtk_widget_queue_draw_area(GTK_WIDGET(display),
1039                                x + d->mx, y + d->my, w, h);
1040 }
1041
1042 static void cursor_set(SpiceCursorChannel *channel,
1043                        gint width, gint height, gint hot_x, gint hot_y,
1044                        gpointer rgba, gpointer data)
1045 {
1046     SpiceDisplay *display = data;
1047     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
1048     GdkDrawable *window;
1049     GdkDisplay *gtkdpy;
1050     GdkPixbuf *pixbuf;
1051
1052     window = gtk_widget_get_window(GTK_WIDGET(display));
1053     if (!window)
1054         return;
1055     gtkdpy = gdk_drawable_get_display(window);
1056
1057     pixbuf = gdk_pixbuf_new_from_data(rgba,
1058                                       GDK_COLORSPACE_RGB,
1059                                       TRUE, 8,
1060                                       width,
1061                                       height,
1062                                       width * 4,
1063                                       NULL, NULL);
1064     d->mouse_cursor = gdk_cursor_new_from_pixbuf(gtkdpy, pixbuf,
1065                                                  hot_x, hot_y);
1066     g_object_unref(pixbuf);
1067     gdk_window_set_cursor(window, d->mouse_cursor);
1068 }
1069
1070 static void cursor_hide(SpiceCursorChannel *channel, gpointer data)
1071 {
1072     SpiceDisplay *display = data;
1073     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
1074     GdkDrawable *window;
1075
1076     window = gtk_widget_get_window(GTK_WIDGET(display));
1077     if (!window)
1078         return;
1079
1080     d->mouse_cursor = gdk_cursor_new(GDK_BLANK_CURSOR);
1081     gdk_window_set_cursor(window, d->mouse_cursor);
1082 }
1083
1084 static void cursor_move(SpiceCursorChannel *channel, gint x, gint y, gpointer data)
1085 {
1086     fprintf(stderr, "%s: TODO (+%d+%d)\n", __FUNCTION__, x, y);
1087 }
1088
1089 static void cursor_reset(SpiceCursorChannel *channel, gpointer data)
1090 {
1091     fprintf(stderr, "%s: TODO\n", __FUNCTION__);
1092 }
1093
1094 static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
1095 {
1096     SpiceDisplay *display = data;
1097     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
1098     int id = spice_channel_id(channel);
1099
1100     if (SPICE_IS_MAIN_CHANNEL(channel)) {
1101         fprintf(stderr, "%s: main channel\n", __FUNCTION__);
1102         d->main = channel;
1103         g_signal_connect(channel, "spice-main-mouse-mode",
1104                          G_CALLBACK(mouse_mode), display);
1105         mouse_mode(channel, spice_main_get_mouse_mode(channel), display);
1106         return;
1107     }
1108
1109     if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
1110         fprintf(stderr, "%s: display channel\n", __FUNCTION__);
1111         if (id != d->channel_id)
1112             return;
1113         d->display = channel;
1114         g_signal_connect(channel, "spice-display-primary-create",
1115                          G_CALLBACK(primary_create), display);
1116         g_signal_connect(channel, "spice-display-primary-destroy",
1117                          G_CALLBACK(primary_destroy), display);
1118         g_signal_connect(channel, "spice-display-invalidate",
1119                          G_CALLBACK(invalidate), display);
1120         spice_channel_connect(channel);
1121         return;
1122     }
1123
1124     if (SPICE_IS_CURSOR_CHANNEL(channel)) {
1125         fprintf(stderr, "%s: cursor channel\n", __FUNCTION__);
1126         if (id != d->channel_id)
1127             return;
1128         d->cursor = SPICE_CURSOR_CHANNEL(channel);
1129         g_signal_connect(channel, "spice-cursor-set",
1130                          G_CALLBACK(cursor_set), display);
1131         g_signal_connect(channel, "spice-cursor-move",
1132                          G_CALLBACK(cursor_move), display);
1133         g_signal_connect(channel, "spice-cursor-hide",
1134                          G_CALLBACK(cursor_hide), display);
1135         g_signal_connect(channel, "spice-cursor-reset",
1136                          G_CALLBACK(cursor_reset), display);
1137         spice_channel_connect(channel);
1138         return;
1139     }
1140
1141     if (SPICE_IS_INPUTS_CHANNEL(channel)) {
1142         fprintf(stderr, "%s: inputs channel\n", __FUNCTION__);
1143         d->inputs = SPICE_INPUTS_CHANNEL(channel);
1144         spice_channel_connect(channel);
1145         return;
1146     }
1147
1148     fprintf(stderr, "%s: unknown channel object\n", __FUNCTION__);
1149     return;
1150 }
1151
1152 GtkWidget *spice_display_new(SpiceSession *session, int id)
1153 {
1154     SpiceDisplay *display;
1155     spice_display *d;
1156     SpiceChannel *channels[16];
1157     int i, n;
1158
1159     display = g_object_new(SPICE_TYPE_DISPLAY, NULL);
1160     d = SPICE_DISPLAY_GET_PRIVATE(display);
1161     d->session = session;
1162     d->channel_id = id;
1163
1164     g_signal_connect(session, "spice-session-channel-new",
1165                      G_CALLBACK(channel_new), display);
1166     n = spice_session_get_channels(session, channels, SPICE_N_ELEMENTS(channels));
1167     for (i = 0; i < n; i++) {
1168         channel_new(session, channels[i], (gpointer*)display);
1169     }
1170
1171     return GTK_WIDGET(display);
1172 }
1173
1174 void spice_display_mouse_ungrab(GtkWidget *widget)
1175 {
1176     try_mouse_ungrab(widget);
1177 }
1178
1179 void spice_display_copy_to_guest(GtkWidget *widget)
1180 {
1181     SpiceDisplay *display = SPICE_DISPLAY(widget);
1182     spice_display *d = SPICE_DISPLAY_GET_PRIVATE(display);
1183
1184     if (d->clip_hasdata && !d->clip_grabbed) {
1185         gtk_clipboard_request_targets(d->clipboard, clipboard_get_targets, display);
1186     }
1187 }
1188
1189 void spice_display_paste_from_guest(GtkWidget *widget)
1190 {
1191     fprintf(stderr, "%s: TODO\n", __FUNCTION__);
1192 }