spice-widget.c 111 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
   Copyright (C) 2010 Red Hat, Inc.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
17
#include "config.h"
18
19

#include <math.h>
20
#include <glib.h>
21
#include <gdk/gdk.h>
22
#include <glib/gi18n-lib.h>
23

24
#ifdef HAVE_X11_XKBLIB_H
25
26
#include <X11/XKBlib.h>
#endif
27
28
29
#ifdef GDK_WINDOWING_X11
#include <X11/Xlib.h>
#include <gdk/gdkx.h>
Frediano Ziglio's avatar
Frediano Ziglio committed
30
31
32
#ifdef HAVE_LIBVA
#include <va/va_x11.h>
#endif
33
#endif
34
35
#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/gdkwayland.h>
36
37
38
39
#ifdef HAVE_WAYLAND_PROTOCOLS
#include "pointer-constraints-unstable-v1-client-protocol.h"
#include "relative-pointer-unstable-v1-client-protocol.h"
#include "wayland-extensions.h"
40
#endif
41
42
#endif

43
#ifdef G_OS_WIN32
44
#include <windows.h>
45
#include <dinput.h>
46
#include <ime.h>
47
#include <gdk/gdkwin32.h>
48
49
50
#ifndef MAPVK_VK_TO_VSC /* may be undefined in older mingw-headers */
#define MAPVK_VK_TO_VSC 0
#endif
51
#endif
52

53
#include "spice-widget.h"
54
#include "spice-widget-priv.h"
55
#include "spice-gtk-session-priv.h"
56
#include "vncdisplaykeymap.h"
57
#include "spice-grabsequence-priv.h"
58

59

60
61
62
63
64
65
/**
 * SECTION:spice-widget
 * @short_description: a GTK display widget
 * @title: Spice Display
 * @section_id:
 * @stability: Stable
66
 * @include: spice-client-gtk.h
67
 *
68
69
70
71
72
73
74
75
76
77
78
79
80
81
 * A GTK widget that displays a SPICE server. It sends keyboard/mouse
 * events and can also share clipboard...
 *
 * Arbitrary key events can be sent thanks to spice_display_send_keys().
 *
 * The widget will optionally grab the keyboard and the mouse when
 * focused if the properties #SpiceDisplay:grab-keyboard and
 * #SpiceDisplay:grab-mouse are #TRUE respectively.  It can be
 * ungrabbed with spice_display_mouse_ungrab(), and by setting a key
 * combination with spice_display_set_grab_keys().
 *
 * Finally, spice_display_get_pixbuf() will take a screenshot of the
 * current display and return an #GdkPixbuf (that you can then easily
 * save to disk).
82
83
 */

84
G_DEFINE_TYPE_WITH_PRIVATE(SpiceDisplay, spice_display, GTK_TYPE_EVENT_BOX)
85
86
87
88

/* Properties */
enum {
    PROP_0,
89
90
    PROP_SESSION,
    PROP_CHANNEL_ID,
91
92
93
    PROP_KEYBOARD_GRAB,
    PROP_MOUSE_GRAB,
    PROP_RESIZE_GUEST,
Marc-André Lureau's avatar
Marc-André Lureau committed
94
    PROP_SCALING,
95
    PROP_ONLY_DOWNSCALE,
96
    PROP_DISABLE_INPUTS,
97
    PROP_ZOOM_LEVEL,
98
    PROP_MONITOR_ID,
99
    PROP_KEYPRESS_DELAY,
100
    PROP_READY
101
102
103
104
};

/* Signals */
enum {
105
    SPICE_DISPLAY_MOUSE_GRAB,
106
    SPICE_DISPLAY_KEYBOARD_GRAB,
107
    SPICE_DISPLAY_GRAB_KEY_PRESSED,
108
109
110
    SPICE_DISPLAY_LAST_SIGNAL,
};

111
112
#define DEFAULT_KEYPRESS_DELAY 100

113
114
static guint signals[SPICE_DISPLAY_LAST_SIGNAL];

115
#ifdef G_OS_WIN32
116
static HWND win32_window = NULL;
117
118
#endif

119
static void update_keyboard_grab(SpiceDisplay *display);
120
121
static void try_keyboard_grab(SpiceDisplay *display);
static void try_keyboard_ungrab(SpiceDisplay *display);
122
static void update_mouse_grab(SpiceDisplay *display);
123
124
static void try_mouse_grab(SpiceDisplay *display);
static void try_mouse_ungrab(SpiceDisplay *display);
125
static void recalc_geometry(GtkWidget *widget);
126
127
static void channel_new(SpiceSession *s, SpiceChannel *channel, SpiceDisplay *display);
static void channel_destroy(SpiceSession *s, SpiceChannel *channel, SpiceDisplay *display);
128
static void cursor_invalidate(SpiceDisplay *display);
129
static bool egl_enabled(SpiceDisplayPrivate *d);
130
static void update_area(SpiceDisplay *display, gint x, gint y, gint width, gint height);
131
static void release_keys(SpiceDisplay *display);
132
static void size_allocate(GtkWidget *widget, GtkAllocation *conf, gpointer data);
Marc-André Lureau's avatar
Marc-André Lureau committed
133
static gboolean draw_event(GtkWidget *widget, cairo_t *cr, gpointer data);
134
static void update_size_request(SpiceDisplay *display);
Pavel Grunt's avatar
Pavel Grunt committed
135
static GdkDevice *spice_gdk_window_get_pointing_device(GdkWindow *window);
136
137
static void gst_size_allocate(GtkWidget *widget, GdkRectangle *a, gpointer data);
static gboolean gst_draw_event(GtkWidget *widget, cairo_t *cr, gpointer data);
138
139
140
141
142
143
144
145
146

/* ---------------------------------------------------------------- */

static void spice_display_get_property(GObject    *object,
                                       guint       prop_id,
                                       GValue     *value,
                                       GParamSpec *pspec)
{
    SpiceDisplay *display = SPICE_DISPLAY(object);
147
    SpiceDisplayPrivate *d = display->priv;
148
149

    switch (prop_id) {
150
151
152
153
154
155
    case PROP_SESSION:
        g_value_set_object(value, d->session);
        break;
    case PROP_CHANNEL_ID:
        g_value_set_int(value, d->channel_id);
        break;
156
157
158
    case PROP_MONITOR_ID:
        g_value_set_int(value, d->monitor_id);
        break;
159
160
    case PROP_KEYBOARD_GRAB:
        g_value_set_boolean(value, d->keyboard_grab_enable);
Marc-André Lureau's avatar
Marc-André Lureau committed
161
        break;
162
163
    case PROP_MOUSE_GRAB:
        g_value_set_boolean(value, d->mouse_grab_enable);
Marc-André Lureau's avatar
Marc-André Lureau committed
164
        break;
165
166
    case PROP_RESIZE_GUEST:
        g_value_set_boolean(value, d->resize_guest_enable);
Marc-André Lureau's avatar
Marc-André Lureau committed
167
        break;
Marc-André Lureau's avatar
Marc-André Lureau committed
168
169
    case PROP_SCALING:
        g_value_set_boolean(value, d->allow_scaling);
170
171
172
173
        break;
    case PROP_ONLY_DOWNSCALE:
        g_value_set_boolean(value, d->only_downscale);
        break;
174
175
    case PROP_DISABLE_INPUTS:
        g_value_set_boolean(value, d->disable_inputs);
176
        break;
177
178
179
    case PROP_ZOOM_LEVEL:
        g_value_set_int(value, d->zoom_level);
        break;
180
181
182
    case PROP_READY:
        g_value_set_boolean(value, d->ready);
        break;
183
184
185
    case PROP_KEYPRESS_DELAY:
        g_value_set_uint(value, d->keypress_delay);
        break;
186
    default:
Marc-André Lureau's avatar
Marc-André Lureau committed
187
188
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
189
190
191
    }
}

192
193
static void scaling_updated(SpiceDisplay *display)
{
194
    SpiceDisplayPrivate *d = display->priv;
195
196
197
    GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(display));

    recalc_geometry(GTK_WIDGET(display));
198
    if (d->canvas.surface && window) { /* if not yet shown */
199
        gtk_widget_queue_draw(GTK_WIDGET(display));
200
    }
201
    update_size_request(display);
202
203
204
205
}

static void update_size_request(SpiceDisplay *display)
{
206
    SpiceDisplayPrivate *d = display->priv;
207
    gint reqwidth, reqheight;
208
    gint scale_factor = 1;
209

210
    if (d->resize_guest_enable || d->allow_scaling) {
211
212
213
        reqwidth = 640;
        reqheight = 480;
    } else {
214
215
        reqwidth = d->area.width;
        reqheight = d->area.height;
216
217
    }

218
219
220
221
222
223
224
    if (egl_enabled(d)) {
        scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(display));
    }

    reqwidth /= scale_factor;
    reqheight /= scale_factor;

225
226
227
228
    gtk_widget_set_size_request(GTK_WIDGET(display), reqwidth, reqheight);
    recalc_geometry(GTK_WIDGET(display));
}

229
230
static void update_keyboard_focus(SpiceDisplay *display, gboolean state)
{
231
    SpiceDisplayPrivate *d = display->priv;
232
233

    d->keyboard_have_focus = state;
234
    spice_gtk_session_set_keyboard_has_focus(d->gtk_session, state);
235
236
237
238
239
240
241
242
243
244
245

    /* keyboard grab gets inhibited by usb-device-manager when it is
       in the process of redirecting a usb-device (as this may show a
       policykit dialog). Making autoredir/automount setting changes while
       this is happening is not a good idea! */
    if (d->keyboard_grab_inhibit)
        return;

    spice_gtk_session_request_auto_usbredir(d->gtk_session, state);
}

246
247
248
249
250
251
252
253
254
255
256
257
258
static gint get_display_id(SpiceDisplay *display)
{
    SpiceDisplayPrivate *d = display->priv;

    /* supported monitor_id only with display channel #0 */
    if (d->channel_id == 0 && d->monitor_id >= 0)
        return d->monitor_id;

    g_return_val_if_fail(d->monitor_id <= 0, -1);

    return d->channel_id;
}

259
260
static bool egl_enabled(SpiceDisplayPrivate *d)
{
261
#if HAVE_EGL
262
263
264
265
266
267
    return d->egl.enabled;
#else
    return false;
#endif
}

268
269
static void update_ready(SpiceDisplay *display)
{
270
    SpiceDisplayPrivate *d = display->priv;
271
    gboolean ready = FALSE;
272

273
274
275
    if (gtk_stack_get_visible_child(d->stack) == d->label) {
        ready = true;
    }
276
    if (d->monitor_ready) {
277
        ready = egl_enabled(d) || d->mark != 0;
278
    }
279
280
281
282
283
284
    /* If the 'resize-guest' property is set, the application expects spice-gtk
     * to manage the size and state of the displays, so update the 'enabled'
     * state here. If 'resize-guest' is false, we can assume that the
     * application will manage the state of the displays.
     */
    if (d->resize_guest_enable) {
285
        spice_main_channel_update_display_enabled(d->main, get_display_id(display), ready, TRUE);
286
287
    }

288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
    if (d->ready == ready)
        return;

    if (ready && gtk_widget_get_window(GTK_WIDGET(display)))
        gtk_widget_queue_draw(GTK_WIDGET(display));

    d->ready = ready;
    g_object_notify(G_OBJECT(display), "ready");
}

static void set_monitor_ready(SpiceDisplay *self, gboolean ready)
{
    SpiceDisplayPrivate *d = self->priv;

    d->monitor_ready = ready;
    update_ready(self);
}

306
307
G_GNUC_INTERNAL
void spice_display_widget_update_monitor_area(SpiceDisplay *display)
308
{
309
    SpiceDisplayPrivate *d = display->priv;
310
    SpiceDisplayMonitorConfig *cfg, *c = NULL;
311
    GArray *monitors = NULL;
312
    int i;
313

314
    DISPLAY_DEBUG(display, "update monitor area");
315
316
    if (d->monitor_id < 0)
        goto whole;
317
318

    g_object_get(d->display, "monitors", &monitors, NULL);
319
    for (i = 0; monitors != NULL && i < monitors->len; i++) {
320
        cfg = &g_array_index(monitors, SpiceDisplayMonitorConfig, i);
321
        if (cfg->id == d->monitor_id) {
322
323
           c = cfg;
           break;
324
325
326
        }
    }
    if (c == NULL) {
327
        DISPLAY_DEBUG(display, "update monitor: no monitor %d", d->monitor_id);
328
        set_monitor_ready(display, false);
329
330
        if (spice_channel_test_capability(SPICE_CHANNEL(d->display),
                                          SPICE_DISPLAY_CAP_MONITORS_CONFIG)) {
331
            DISPLAY_DEBUG(display, "waiting until MonitorsConfig is received");
332
            g_clear_pointer(&monitors, g_array_unref);
333
334
335
336
337
338
339
            return;
        }
        goto whole;
    }

    if (c->surface_id != 0) {
        g_warning("FIXME: only support monitor config with primary surface 0, "
340
                  "but given config surface %u", c->surface_id);
341
342
343
        goto whole;
    }

344
    /* If only one head on this monitor, update the whole area */
345
    if (monitors->len == 1 && !egl_enabled(d)) {
346
347
348
349
        update_area(display, 0, 0, c->width, c->height);
    } else {
        update_area(display, c->x, c->y, c->width, c->height);
    }
350
    g_clear_pointer(&monitors, g_array_unref);
351
352
353
    return;

whole:
354
    g_clear_pointer(&monitors, g_array_unref);
355
    /* by display whole surface */
356
    update_area(display, 0, 0, d->canvas.width, d->canvas.height);
357
    set_monitor_ready(display, true);
358
359
}

360
361
362
363
364
365
366
367
368
369
static void
spice_display_set_keypress_delay(SpiceDisplay *display, guint delay)
{
    SpiceDisplayPrivate *d = display->priv;
    const gchar *env = g_getenv("SPICE_KEYPRESS_DELAY");

    if (env != NULL)
        delay = strtoul(env, NULL, 10);

    if (d->keypress_delay != delay) {
370
        DISPLAY_DEBUG(display, "keypress-delay is set to %u ms", delay);
371
372
373
374
375
        d->keypress_delay = delay;
        g_object_notify(G_OBJECT(display), "keypress-delay");
    }
}

376
377
378
379
380
381
static void spice_display_set_property(GObject      *object,
                                       guint         prop_id,
                                       const GValue *value,
                                       GParamSpec   *pspec)
{
    SpiceDisplay *display = SPICE_DISPLAY(object);
382
    SpiceDisplayPrivate *d = display->priv;
383
384

    switch (prop_id) {
385
    case PROP_SESSION:
386
387
        g_warn_if_fail(d->session == NULL);
        d->session = g_value_dup_object(value);
388
        d->gtk_session = spice_gtk_session_get(d->session);
389
390
391
        spice_g_signal_connect_object(d->gtk_session, "notify::pointer-grabbed",
                                      G_CALLBACK(cursor_invalidate), object,
                                      G_CONNECT_SWAPPED);
392
393
394
395
        break;
    case PROP_CHANNEL_ID:
        d->channel_id = g_value_get_int(value);
        break;
396
397
    case PROP_MONITOR_ID:
        d->monitor_id = g_value_get_int(value);
398
        if (d->display) /* if constructed */
399
            spice_display_widget_update_monitor_area(display);
400
        break;
401
402
    case PROP_KEYBOARD_GRAB:
        d->keyboard_grab_enable = g_value_get_boolean(value);
403
        update_keyboard_grab(display);
404
405
406
        break;
    case PROP_MOUSE_GRAB:
        d->mouse_grab_enable = g_value_get_boolean(value);
407
        update_mouse_grab(display);
408
409
410
        break;
    case PROP_RESIZE_GUEST:
        d->resize_guest_enable = g_value_get_boolean(value);
411
        update_ready(display);
412
        update_size_request(display);
413
        break;
Marc-André Lureau's avatar
Marc-André Lureau committed
414
415
    case PROP_SCALING:
        d->allow_scaling = g_value_get_boolean(value);
416
        scaling_updated(display);
Marc-André Lureau's avatar
Marc-André Lureau committed
417
        break;
418
419
420
421
    case PROP_ONLY_DOWNSCALE:
        d->only_downscale = g_value_get_boolean(value);
        scaling_updated(display);
        break;
422
423
424
425
426
427
    case PROP_DISABLE_INPUTS:
        d->disable_inputs = g_value_get_boolean(value);
        gtk_widget_set_can_focus(GTK_WIDGET(display), !d->disable_inputs);
        update_keyboard_grab(display);
        update_mouse_grab(display);
        break;
428
429
430
431
    case PROP_ZOOM_LEVEL:
        d->zoom_level = g_value_get_int(value);
        scaling_updated(display);
        break;
432
    case PROP_KEYPRESS_DELAY:
433
        spice_display_set_keypress_delay(display, g_value_get_uint(value));
434
        break;
435
436
437
438
439
440
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

441
442
443
444
445
static void session_inhibit_keyboard_grab_changed(GObject    *gobject,
                                                  GParamSpec *pspec,
                                                  gpointer    user_data)
{
    SpiceDisplay *display = user_data;
446
    SpiceDisplayPrivate *d = display->priv;
447
448
449

    g_object_get(d->session, "inhibit-keyboard-grab",
                 &d->keyboard_grab_inhibit, NULL);
450
    update_keyboard_grab(display);
451
    update_mouse_grab(display);
452
453
}

454
static void spice_display_dispose(GObject *obj)
455
{
Gerd Hoffmann's avatar
Gerd Hoffmann committed
456
    SpiceDisplay *display = SPICE_DISPLAY(obj);
457
    SpiceDisplayPrivate *d = display->priv;
Gerd Hoffmann's avatar
Gerd Hoffmann committed
458

459
    DISPLAY_DEBUG(display, "spice display dispose");
Gerd Hoffmann's avatar
Gerd Hoffmann committed
460

461
    spice_cairo_image_destroy(display);
462
463
    g_clear_object(&d->session);
    d->gtk_session = NULL;
464

465
466
467
468
469
    if (d->key_delayed_id) {
        g_source_remove(d->key_delayed_id);
        d->key_delayed_id = 0;
    }

470
    G_OBJECT_CLASS(spice_display_parent_class)->dispose(obj);
471
472
}

473
474
static void spice_display_finalize(GObject *obj)
{
475
    SpiceDisplay *display = SPICE_DISPLAY(obj);
476
    SpiceDisplayPrivate *d = display->priv;
477

478
    DISPLAY_DEBUG(display, "Finalize spice display");
479

Pavel Grunt's avatar
Pavel Grunt committed
480
481
    g_clear_pointer(&d->grabseq, spice_grab_sequence_free);
    g_clear_pointer(&d->activeseq, g_free);
482

Pavel Grunt's avatar
Pavel Grunt committed
483
484
485
    g_clear_object(&d->show_cursor);
    g_clear_object(&d->mouse_cursor);
    g_clear_object(&d->mouse_pixbuf);
486
    cairo_surface_destroy(d->cursor_surface);
487

488
489
490
    G_OBJECT_CLASS(spice_display_parent_class)->finalize(obj);
}

491
static GdkCursor* spice_display_get_blank_cursor(SpiceDisplay *display)
492
{
493
494
495
    GdkDisplay *gdk_display;
    const gchar *cursor_name;
    GdkWindow *gdk_window = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));
496

497
498
499
500
501
502
503
    if (gdk_window == NULL)
        return NULL;

    gdk_display = gdk_window_get_display(gdk_window);
    cursor_name = g_getenv("SPICE_DEBUG_CURSOR") ? "crosshair" : "none";

    return gdk_cursor_new_from_name(gdk_display, cursor_name);
504
505
}

Marc-André Lureau's avatar
Marc-André Lureau committed
506
507
508
static gboolean grab_broken(SpiceDisplay *self, GdkEventGrabBroken *event,
                            gpointer user_data G_GNUC_UNUSED)
{
509
    GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
510
511
512
513
    DISPLAY_DEBUG(self, "%s (implicit: %d, keyboard: %d)", __FUNCTION__,
                  event->implicit, event->keyboard);
    DISPLAY_DEBUG(self, "%s (SpiceDisplay::GdkWindow %p, event->grab_window: %p)",
                  __FUNCTION__, window, event->grab_window);
514
515
516
517
518
519
520
521
    if (window == event->grab_window) {
        /* ignore grab-broken event moving the grab to GtkEventBox::window
         * (from GtkEventBox::event_window) as we initially called
         * gdk_pointer_grab() on GtkEventBox::window, see
         * https://bugzilla.gnome.org/show_bug.cgi?id=769635
         */
        return false;
    }
Marc-André Lureau's avatar
Marc-André Lureau committed
522
523

    if (event->keyboard) {
524
        try_keyboard_ungrab(self);
525
        release_keys(self);
Marc-André Lureau's avatar
Marc-André Lureau committed
526
527
    }

528
529
530
531
532
    /* always release mouse when grab broken, this could be more
       generally placed in keyboard_ungrab(), but one might worry of
       breaking someone else code. */
    try_mouse_ungrab(self);

Marc-André Lureau's avatar
Marc-André Lureau committed
533
534
535
    return false;
}

536
537
538
539
540
541
542
static void file_transfer_callback(GObject *source_object,
                                   GAsyncResult *result,
                                   gpointer user_data G_GNUC_UNUSED)
{
    SpiceMainChannel *channel = SPICE_MAIN_CHANNEL(source_object);
    GError *error = NULL;

543
    if (spice_main_channel_file_copy_finish(channel, result, &error))
544
545
546
547
548
549
550
551
552
553
        return;

    if (error != NULL && error->message != NULL)
        g_warning("File transfer failed with error: %s", error->message);
    else
        g_warning("File transfer failed");

    g_clear_error(&error);
}

554
555
556
557
558
559
560
561
562
static void drag_data_received_callback(SpiceDisplay *self,
                                        GdkDragContext *drag_context,
                                        gint x,
                                        gint y,
                                        GtkSelectionData *data,
                                        guint info,
                                        guint time,
                                        gpointer *user_data)
{
563
    const guchar *buf;
564
565
    gchar **file_urls;
    int n_files;
566
    SpiceDisplayPrivate *d = self->priv;
567
568
569
570
571
572
    int i = 0;
    GFile **files;

    /* We get a buf like:
     * file:///root/a.txt\r\nfile:///root/b.txt\r\n
     */
573
    DISPLAY_DEBUG(self, "%s: drag a file", __FUNCTION__);
574
    buf = gtk_selection_data_get_data(data);
575
576
    g_return_if_fail(buf != NULL);

577
    file_urls = g_uri_list_extract_uris((const gchar*)buf);
578
    n_files = g_strv_length(file_urls);
579
    files = g_new0(GFile*, n_files + 1);
580
581
582
583
584
    for (i = 0; i < n_files; i++) {
        files[i] = g_file_new_for_uri(file_urls[i]);
    }
    g_strfreev(file_urls);

585
586
    spice_main_channel_file_copy_async(d->main, files, 0, NULL, NULL, NULL, file_transfer_callback,
                                       NULL);
587
588
589
590
591
592
593
594
    for (i = 0; i < n_files; i++) {
        g_object_unref(files[i]);
    }
    g_free(files);

    gtk_drag_finish(drag_context, TRUE, FALSE, time);
}

595
596
static void grab_notify(SpiceDisplay *display, gboolean was_grabbed)
{
597
    DISPLAY_DEBUG(display, "grab notify %d", was_grabbed);
598
599
600
601
602

    if (was_grabbed == FALSE)
        release_keys(display);
}

603
#if HAVE_EGL
Marc-André Lureau's avatar
Marc-André Lureau committed
604
605
606
607
608
609
610
611
612
static gboolean
gl_area_render(GtkGLArea *area, GdkGLContext *context, gpointer user_data)
{
    SpiceDisplay *display = SPICE_DISPLAY(user_data);
    SpiceDisplayPrivate *d = display->priv;

    spice_egl_update_display(display);
    glFlush();
    if (d->egl.call_draw_done) {
613
        spice_display_channel_gl_draw_done(d->display);
Marc-André Lureau's avatar
Marc-André Lureau committed
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
        d->egl.call_draw_done = FALSE;
    }

    return TRUE;
}

static void
gl_area_realize(GtkGLArea *area, gpointer user_data)
{
    SpiceDisplay *display = SPICE_DISPLAY(user_data);
    GError *err = NULL;

    gtk_gl_area_make_current(area);
    if (gtk_gl_area_get_error(area) != NULL)
        return;

    if (!spice_egl_init(display, &err)) {
        g_critical("egl init failed: %s", err->message);
        g_clear_error(&err);
    }
}
635
#endif
Marc-André Lureau's avatar
Marc-André Lureau committed
636

Marc-André Lureau's avatar
Marc-André Lureau committed
637
638
639
static void
drawing_area_realize(GtkWidget *area, gpointer user_data)
{
Pavel Grunt's avatar
Pavel Grunt committed
640
#if defined(GDK_WINDOWING_X11) && defined(HAVE_EGL)
Marc-André Lureau's avatar
Marc-André Lureau committed
641
642
    SpiceDisplay *display = SPICE_DISPLAY(user_data);

643
    if (GDK_IS_X11_DISPLAY(gdk_display_get_default()) &&
644
        spice_display_channel_get_gl_scanout(display->priv->display) != NULL) {
645
        spice_display_widget_gl_scanout(display);
646
    }
Marc-André Lureau's avatar
Marc-André Lureau committed
647
#endif
Marc-André Lureau's avatar
Marc-André Lureau committed
648
649
}

650
651
652
static void spice_display_init(SpiceDisplay *display)
{
    GtkWidget *widget = GTK_WIDGET(display);
Marc-André Lureau's avatar
Marc-André Lureau committed
653
    GtkWidget *area;
654
    SpiceDisplayPrivate *d;
655
    GtkTargetEntry targets = { "text/uri-list", 0, 0 };
656

657
    d = display->priv = spice_display_get_instance_private(display);
658
659
    d->stack = GTK_STACK(gtk_stack_new());
    gtk_container_add(GTK_CONTAINER(display), GTK_WIDGET(d->stack));
Marc-André Lureau's avatar
Marc-André Lureau committed
660
    area = gtk_drawing_area_new();
661

Marc-André Lureau's avatar
Marc-André Lureau committed
662
663
664
665
    g_object_connect(area,
                     "signal::draw", draw_event, display,
                     "signal::realize", drawing_area_realize, display,
                     NULL);
666
667
    gtk_stack_add_named(d->stack, area, "draw-area");
    gtk_stack_set_visible_child(d->stack, area);
Marc-André Lureau's avatar
Marc-André Lureau committed
668

669
#if HAVE_EGL
Marc-André Lureau's avatar
Marc-André Lureau committed
670
671
672
673
674
675
676
    area = gtk_gl_area_new();
    gtk_gl_area_set_required_version(GTK_GL_AREA(area), 3, 2);
    gtk_gl_area_set_auto_render(GTK_GL_AREA(area), false);
    g_object_connect(area,
                     "signal::render", gl_area_render, display,
                     "signal::realize", gl_area_realize, display,
                     NULL);
677
    gtk_stack_add_named(d->stack, area, "gl-area");
678
#endif
679
680
    area = gtk_drawing_area_new();
    gtk_stack_add_named(d->stack, area, "gst-area");
681
682
683
684
    g_object_connect(area,
                     "signal::draw", gst_draw_event, display,
                     "signal::size-allocate", gst_size_allocate, display,
                     NULL);
685

686
687
688
    d->label = gtk_label_new(NULL);
    gtk_label_set_selectable(GTK_LABEL(d->label), true);
    gtk_stack_add_named(d->stack, d->label, "label");
689

690
    gtk_widget_show_all(widget);
Marc-André Lureau's avatar
Marc-André Lureau committed
691

Marc-André Lureau's avatar
Marc-André Lureau committed
692
    g_signal_connect(display, "grab-broken-event", G_CALLBACK(grab_broken), NULL);
693
694
    g_signal_connect(display, "grab-notify", G_CALLBACK(grab_notify), NULL);

695
696
697
    gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, &targets, 1, GDK_ACTION_COPY);
    g_signal_connect(display, "drag-data-received",
                     G_CALLBACK(drag_data_received_callback), NULL);
698
    g_signal_connect(display, "size-allocate", G_CALLBACK(size_allocate), NULL);
699

700
701
702
703
704
705
706
    gtk_widget_add_events(widget,
                          GDK_POINTER_MOTION_MASK |
                          GDK_BUTTON_PRESS_MASK |
                          GDK_BUTTON_RELEASE_MASK |
                          GDK_BUTTON_MOTION_MASK |
                          GDK_ENTER_NOTIFY_MASK |
                          GDK_LEAVE_NOTIFY_MASK |
707
                          GDK_KEY_PRESS_MASK |
708
709
                          /* on Wayland, only smooth-scroll events are emitted */
                          GDK_SMOOTH_SCROLL_MASK |
710
                          GDK_SCROLL_MASK);
711
    gtk_widget_set_can_focus(widget, true);
712
713
    gtk_event_box_set_above_child(GTK_EVENT_BOX(widget), true);

714
    d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
715
    d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
716
717
718
719
720

#ifdef HAVE_WAYLAND_PROTOCOLS
    if GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(widget))
        spice_wayland_extensions_init(widget);
#endif
721
722
}

723
724
static void
spice_display_constructed(GObject *gobject)
725
726
727
728
729
730
{
    SpiceDisplay *display;
    SpiceDisplayPrivate *d;
    GList *list;
    GList *it;

731
    display = SPICE_DISPLAY(gobject);
732
    d = display->priv;
733
734
735
736

    if (!d->session)
        g_error("SpiceDisplay constructed without a session");

737
738
739
740
    spice_g_signal_connect_object(d->session, "channel-new",
                                  G_CALLBACK(channel_new), display, 0);
    spice_g_signal_connect_object(d->session, "channel-destroy",
                                  G_CALLBACK(channel_destroy), display, 0);
741
742
    list = spice_session_get_channels(d->session);
    for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
743
        if (SPICE_IS_MAIN_CHANNEL(it->data)) {
744
            channel_new(d->session, it->data, display);
745
746
747
748
749
            break;
        }
    }
    for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
        if (!SPICE_IS_MAIN_CHANNEL(it->data))
750
            channel_new(d->session, it->data, display);
751
752
753
    }
    g_list_free(list);

754
755
756
    spice_g_signal_connect_object(d->session, "notify::inhibit-keyboard-grab",
                                  G_CALLBACK(session_inhibit_keyboard_grab_changed),
                                  display, 0);
757
}
758

759
760
/**
 * spice_display_set_grab_keys:
761
762
 * @display: the display widget
 * @seq: (transfer none): key sequence
763
764
765
766
 *
 * Set the key combination to grab/ungrab the keyboard. The default is
 * "Control L + Alt L".
 **/
767
void spice_display_set_grab_keys(SpiceDisplay *display, SpiceGrabSequence *seq)
768
{
769
770
771
772
773
    SpiceDisplayPrivate *d;

    g_return_if_fail(SPICE_IS_DISPLAY(display));

    d = display->priv;
774
775
776
    g_return_if_fail(d != NULL);

    if (d->grabseq) {
777
        spice_grab_sequence_free(d->grabseq);
778
779
    }
    if (seq)
780
        d->grabseq = spice_grab_sequence_copy(seq);
781
    else
782
        d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
783
    g_free(d->activeseq);
784
785
786
    d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
}

787
#ifdef G_OS_WIN32
788
789
static LRESULT CALLBACK keyboard_hook_cb(int code, WPARAM wparam, LPARAM lparam)
{
790
    if  (win32_window && code == HC_ACTION && wparam != WM_KEYUP) {
791
792
793
794
795
        KBDLLHOOKSTRUCT *hooked = (KBDLLHOOKSTRUCT*)lparam;
        DWORD dwmsg = (hooked->flags << 24) | (hooked->scanCode << 16) | 1;

        if (hooked->vkCode == VK_NUMLOCK || hooked->vkCode == VK_RSHIFT) {
            dwmsg &= ~(1 << 24);
796
            SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
797
798
799
800
801
802
803
804
805
806
807
        }
        switch (hooked->vkCode) {
        case VK_CAPITAL:
        case VK_SCROLL:
        case VK_NUMLOCK:
        case VK_LSHIFT:
        case VK_RSHIFT:
        case VK_RCONTROL:
        case VK_LMENU:
        case VK_RMENU:
            break;
808
809
810
811
812
813
814
        case VK_LCONTROL:
            /* When pressing AltGr, an extra VK_LCONTROL with a special
             * scancode with bit 9 set is sent. Let's ignore the extra
             * VK_LCONTROL, as that will make AltGr misbehave. */
            if (hooked->scanCode & 0x200)
                return 1;
            break;
815
        default:
816
            SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
817
818
819
820
821
822
823
            return 1;
        }
    }
    return CallNextHookEx(NULL, code, wparam, lparam);
}
#endif

824
825
/**
 * spice_display_get_grab_keys:
826
 * @display: the display widget
827
 *
828
829
 * Finds the current grab key combination for the @display.
 *
830
 * Returns: (transfer none): the current grab key combination.
831
 **/
832
SpiceGrabSequence *spice_display_get_grab_keys(SpiceDisplay *display)
833
{
834
835
836
837
838
    SpiceDisplayPrivate *d;

    g_return_val_if_fail(SPICE_IS_DISPLAY(display), NULL);

    d = display->priv;
839
840
841
842
843
    g_return_val_if_fail(d != NULL, NULL);

    return d->grabseq;
}

844
845
846
847
848
849
850
static GdkSeat *spice_display_get_default_seat(SpiceDisplay *display)
{
    GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(display));
    GdkDisplay *gdk_display = gdk_window_get_display(window);
    return gdk_display_get_default_seat(gdk_display);
}

851
852
853
/* FIXME: gdk_keyboard_grab/ungrab() is deprecated */
G_GNUC_BEGIN_IGNORE_DEPRECATIONS

854
static void try_keyboard_grab(SpiceDisplay *display)
855
{
856
    GtkWidget *widget = GTK_WIDGET(display);
857
    SpiceDisplayPrivate *d = display->priv;
858
    GdkGrabStatus status;
859

860
861
    if (g_getenv("SPICE_NOGRAB"))
        return;
862
    if (d->disable_inputs)
863
864
        return;

865
866
    if (d->keyboard_grab_inhibit)
        return;
867
868
    if (!d->keyboard_grab_enable)
        return;
869
870
    if (d->keyboard_grab_active)
        return;
871
872
873
874
875
876
#ifdef G_OS_WIN32
    if (!d->keyboard_have_focus)
        return;
    if (!d->mouse_have_pointer)
        return;
#else
877
    if (!spice_gtk_session_get_keyboard_has_focus(d->gtk_session))
878
        return;
879
    if (!spice_gtk_session_get_mouse_has_pointer(d->gtk_session))
880
        return;
881
#endif
882
883
    if (d->keyboard_grab_released)
        return;
884

885
886
    g_return_if_fail(gtk_widget_is_focus(widget));

887
    DISPLAY_DEBUG(display, "grab keyboard");
888
    gtk_widget_grab_focus(widget);
889

890
#ifdef G_OS_WIN32
891
892
893
894
    if (d->keyboard_hook == NULL)
        d->keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook_cb,
                                            GetModuleHandle(NULL), 0);
    g_warn_if_fail(d->keyboard_hook != NULL);
895
#endif
896
897
898
899
900
901
902
903
    status = gdk_seat_grab(spice_display_get_default_seat(display),
                           gtk_widget_get_window(widget),
                           GDK_SEAT_CAPABILITY_KEYBOARD,
                           FALSE,
                           NULL,
                           NULL,
                           NULL,
                           NULL);
904
    if (status != GDK_GRAB_SUCCESS) {
905
        g_warning("keyboard grab failed %u", status);
906
        d->keyboard_grab_active = false;
907
    } else {
908
        d->keyboard_grab_active = true;
909
910
        g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, true);
    }
911
912
}

913
static void ungrab_keyboard(SpiceDisplay *display)
914
{
915
916
917
918
919
920
921
922
    GdkSeat *seat = spice_display_get_default_seat(display);
    GdkDevice *keyboard = gdk_seat_get_keyboard(seat);

#ifdef GDK_WINDOWING_WAYLAND
    /* On Wayland, use the GdkSeat API alone.
     * We simply issue a gdk_seat_ungrab() followed immediately by another
     * gdk_seat_grab() on the pointer if the pointer grab is to be kept.
     */
923
    if (GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(GTK_WIDGET(display)))) {
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
        SpiceDisplayPrivate *d = display->priv;

        gdk_seat_ungrab(seat);

        if (d->mouse_grab_active) {
            GdkGrabStatus status;
            GdkCursor *blank = spice_display_get_blank_cursor(display);

            status = gdk_seat_grab(seat,
                                   gtk_widget_get_window(GTK_WIDGET(display)),
                                   GDK_SEAT_CAPABILITY_ALL_POINTING,
                                   TRUE,
                                   blank,
                                   NULL,
                                   NULL,
                                   NULL);
            if (status != GDK_GRAB_SUCCESS) {
                g_warning("pointer grab failed %u", status);
                d->mouse_grab_active = false;
            }
        }

        return;
    }
#endif

950
951
952
953
954
955
956
    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
    /* we want to ungrab just the keyboard - it is not possible using gdk_seat_ungrab().
       See also https://bugzilla.gnome.org/show_bug.cgi?id=780133 */
    gdk_device_ungrab(keyboard, GDK_CURRENT_TIME);
    G_GNUC_END_IGNORE_DEPRECATIONS
}

957
static void try_keyboard_ungrab(SpiceDisplay *display)
958
{
959
    SpiceDisplayPrivate *d = display->priv;
960
    GtkWidget *widget = GTK_WIDGET(display);
961
962
963
964

    if (!d->keyboard_grab_active)
        return;

965
    DISPLAY_DEBUG(display, "ungrab keyboard");
966
    ungrab_keyboard(display);
967
#ifdef G_OS_WIN32
968
969
970
971
972
    // do not use g_clear_pointer as Windows API have different linkage
    if (d->keyboard_hook) {
        UnhookWindowsHookEx(d->keyboard_hook);
        d->keyboard_hook = NULL;
    }
973
#endif
974
    d->keyboard_grab_active = false;
975
    g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, false);
976
}
977
978
G_GNUC_END_IGNORE_DEPRECATIONS

979

980
981
static void update_keyboard_grab(SpiceDisplay *display)
{
982
    SpiceDisplayPrivate *d = display->priv;
983
984
985
986
987
988
989
990
991

    if (d->keyboard_grab_enable &&
        !d->keyboard_grab_inhibit &&
        !d->disable_inputs)
        try_keyboard_grab(display);
    else
        try_keyboard_ungrab(display);
}

992
993
994
static void set_mouse_accel(SpiceDisplay *display, gboolean enabled)
{
#if defined GDK_WINDOWING_X11
995
    SpiceDisplayPrivate *d = display->priv;
996
997
    GdkWindow *w = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));

998
    if (!GDK_IS_X11_DISPLAY(gdk_window_get_display(w))) {
999
        DISPLAY_DEBUG(display, "FIXME: gtk backend is not X11");
1000
        return;