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 void update_area(SpiceDisplay *display, gint x, gint y, gint width, gint height);
130
static void release_keys(SpiceDisplay *display);
131
static void size_allocate(GtkWidget *widget, GtkAllocation *conf, gpointer data);
Marc-André Lureau's avatar
Marc-André Lureau committed
132
static gboolean draw_event(GtkWidget *widget, cairo_t *cr, gpointer data);
133
static void update_size_request(SpiceDisplay *display);
Pavel Grunt's avatar
Pavel Grunt committed
134
static GdkDevice *spice_gdk_window_get_pointing_device(GdkWindow *window);
135
136
static void gst_size_allocate(GtkWidget *widget, GdkRectangle *a, gpointer data);
static gboolean gst_draw_event(GtkWidget *widget, cairo_t *cr, gpointer data);
137
138
139
140
141
142
143
144
145

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

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

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

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

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

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

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

    gtk_widget_set_size_request(GTK_WIDGET(display), reqwidth, reqheight);
    recalc_geometry(GTK_WIDGET(display));
}

220
221
static void update_keyboard_focus(SpiceDisplay *display, gboolean state)
{
222
    SpiceDisplayPrivate *d = display->priv;
223
224

    d->keyboard_have_focus = state;
225
    spice_gtk_session_set_keyboard_has_focus(d->gtk_session, state);
226
227
228
229
230
231
232
233
234
235
236

    /* 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);
}

237
238
239
240
241
242
243
244
245
246
247
248
249
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;
}

250
251
static bool egl_enabled(SpiceDisplayPrivate *d)
{
252
#if HAVE_EGL
253
254
255
256
257
258
    return d->egl.enabled;
#else
    return false;
#endif
}

259
260
static void update_ready(SpiceDisplay *display)
{
261
    SpiceDisplayPrivate *d = display->priv;
262
    gboolean ready = FALSE;
263

264
265
266
    if (gtk_stack_get_visible_child(d->stack) == d->label) {
        ready = true;
    }
267
    if (d->monitor_ready) {
268
        ready = egl_enabled(d) || d->mark != 0;
269
    }
270
271
272
273
274
275
    /* 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) {
276
        spice_main_channel_update_display_enabled(d->main, get_display_id(display), ready, TRUE);
277
278
    }

279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
    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);
}

297
298
G_GNUC_INTERNAL
void spice_display_widget_update_monitor_area(SpiceDisplay *display)
299
{
300
    SpiceDisplayPrivate *d = display->priv;
301
    SpiceDisplayMonitorConfig *cfg, *c = NULL;
302
    GArray *monitors = NULL;
303
    int i;
304

305
    DISPLAY_DEBUG(display, "update monitor area");
306
307
    if (d->monitor_id < 0)
        goto whole;
308
309

    g_object_get(d->display, "monitors", &monitors, NULL);
310
    for (i = 0; monitors != NULL && i < monitors->len; i++) {
311
        cfg = &g_array_index(monitors, SpiceDisplayMonitorConfig, i);
312
        if (cfg->id == d->monitor_id) {
313
314
           c = cfg;
           break;
315
316
317
        }
    }
    if (c == NULL) {
318
        DISPLAY_DEBUG(display, "update monitor: no monitor %d", d->monitor_id);
319
        set_monitor_ready(display, false);
320
321
        if (spice_channel_test_capability(SPICE_CHANNEL(d->display),
                                          SPICE_DISPLAY_CAP_MONITORS_CONFIG)) {
322
            DISPLAY_DEBUG(display, "waiting until MonitorsConfig is received");
323
            g_clear_pointer(&monitors, g_array_unref);
324
325
326
327
328
329
330
            return;
        }
        goto whole;
    }

    if (c->surface_id != 0) {
        g_warning("FIXME: only support monitor config with primary surface 0, "
331
                  "but given config surface %u", c->surface_id);
332
333
334
        goto whole;
    }

335
    /* If only one head on this monitor, update the whole area */
336
    if (monitors->len == 1 && !egl_enabled(d)) {
337
338
339
340
        update_area(display, 0, 0, c->width, c->height);
    } else {
        update_area(display, c->x, c->y, c->width, c->height);
    }
341
    g_clear_pointer(&monitors, g_array_unref);
342
343
344
    return;

whole:
345
    g_clear_pointer(&monitors, g_array_unref);
346
    /* by display whole surface */
347
    update_area(display, 0, 0, d->canvas.width, d->canvas.height);
348
    set_monitor_ready(display, true);
349
350
}

351
352
353
354
355
356
357
358
359
360
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) {
361
        DISPLAY_DEBUG(display, "keypress-delay is set to %u ms", delay);
362
363
364
365
366
        d->keypress_delay = delay;
        g_object_notify(G_OBJECT(display), "keypress-delay");
    }
}

367
368
369
370
371
372
static void spice_display_set_property(GObject      *object,
                                       guint         prop_id,
                                       const GValue *value,
                                       GParamSpec   *pspec)
{
    SpiceDisplay *display = SPICE_DISPLAY(object);
373
    SpiceDisplayPrivate *d = display->priv;
374
375

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

432
433
434
435
436
static void session_inhibit_keyboard_grab_changed(GObject    *gobject,
                                                  GParamSpec *pspec,
                                                  gpointer    user_data)
{
    SpiceDisplay *display = user_data;
437
    SpiceDisplayPrivate *d = display->priv;
438
439
440

    g_object_get(d->session, "inhibit-keyboard-grab",
                 &d->keyboard_grab_inhibit, NULL);
441
    update_keyboard_grab(display);
442
    update_mouse_grab(display);
443
444
}

445
static void spice_display_dispose(GObject *obj)
446
{
Gerd Hoffmann's avatar
Gerd Hoffmann committed
447
    SpiceDisplay *display = SPICE_DISPLAY(obj);
448
    SpiceDisplayPrivate *d = display->priv;
Gerd Hoffmann's avatar
Gerd Hoffmann committed
449

450
    DISPLAY_DEBUG(display, "spice display dispose");
Gerd Hoffmann's avatar
Gerd Hoffmann committed
451

452
    spice_cairo_image_destroy(display);
453
454
    g_clear_object(&d->session);
    d->gtk_session = NULL;
455

456
457
458
459
460
    if (d->key_delayed_id) {
        g_source_remove(d->key_delayed_id);
        d->key_delayed_id = 0;
    }

461
    G_OBJECT_CLASS(spice_display_parent_class)->dispose(obj);
462
463
}

464
465
static void spice_display_finalize(GObject *obj)
{
466
    SpiceDisplay *display = SPICE_DISPLAY(obj);
467
    SpiceDisplayPrivate *d = display->priv;
468

469
    DISPLAY_DEBUG(display, "Finalize spice display");
470

Pavel Grunt's avatar
Pavel Grunt committed
471
472
    g_clear_pointer(&d->grabseq, spice_grab_sequence_free);
    g_clear_pointer(&d->activeseq, g_free);
473

Pavel Grunt's avatar
Pavel Grunt committed
474
475
476
    g_clear_object(&d->show_cursor);
    g_clear_object(&d->mouse_cursor);
    g_clear_object(&d->mouse_pixbuf);
477
    cairo_surface_destroy(d->cursor_surface);
478

479
480
481
    G_OBJECT_CLASS(spice_display_parent_class)->finalize(obj);
}

482
static GdkCursor* spice_display_get_blank_cursor(SpiceDisplay *display)
483
{
484
485
486
    GdkDisplay *gdk_display;
    const gchar *cursor_name;
    GdkWindow *gdk_window = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));
487

488
489
490
491
492
493
494
    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);
495
496
}

Marc-André Lureau's avatar
Marc-André Lureau committed
497
498
499
static gboolean grab_broken(SpiceDisplay *self, GdkEventGrabBroken *event,
                            gpointer user_data G_GNUC_UNUSED)
{
500
    GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
501
502
503
504
    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);
505
506
507
508
509
510
511
512
    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
513
514

    if (event->keyboard) {
515
        try_keyboard_ungrab(self);
516
        release_keys(self);
Marc-André Lureau's avatar
Marc-André Lureau committed
517
518
    }

519
520
521
522
523
    /* 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
524
525
526
    return false;
}

527
528
529
530
531
532
533
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;

534
    if (spice_main_channel_file_copy_finish(channel, result, &error))
535
536
537
538
539
540
541
542
543
544
        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);
}

545
546
547
548
549
550
551
552
553
static void drag_data_received_callback(SpiceDisplay *self,
                                        GdkDragContext *drag_context,
                                        gint x,
                                        gint y,
                                        GtkSelectionData *data,
                                        guint info,
                                        guint time,
                                        gpointer *user_data)
{
554
    const guchar *buf;
555
556
    gchar **file_urls;
    int n_files;
557
    SpiceDisplayPrivate *d = self->priv;
558
559
560
561
562
563
    int i = 0;
    GFile **files;

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

568
    file_urls = g_uri_list_extract_uris((const gchar*)buf);
569
    n_files = g_strv_length(file_urls);
570
    files = g_new0(GFile*, n_files + 1);
571
572
573
574
575
    for (i = 0; i < n_files; i++) {
        files[i] = g_file_new_for_uri(file_urls[i]);
    }
    g_strfreev(file_urls);

576
577
    spice_main_channel_file_copy_async(d->main, files, 0, NULL, NULL, NULL, file_transfer_callback,
                                       NULL);
578
579
580
581
582
583
584
585
    for (i = 0; i < n_files; i++) {
        g_object_unref(files[i]);
    }
    g_free(files);

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

586
587
static void grab_notify(SpiceDisplay *display, gboolean was_grabbed)
{
588
    DISPLAY_DEBUG(display, "grab notify %d", was_grabbed);
589
590
591
592
593

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

594
#if HAVE_EGL
Marc-André Lureau's avatar
Marc-André Lureau committed
595
596
597
598
599
600
601
602
603
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) {
604
        spice_display_channel_gl_draw_done(d->display);
Marc-André Lureau's avatar
Marc-André Lureau committed
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
        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);
    }
}
626
#endif
Marc-André Lureau's avatar
Marc-André Lureau committed
627

Marc-André Lureau's avatar
Marc-André Lureau committed
628
629
630
static void
drawing_area_realize(GtkWidget *area, gpointer user_data)
{
Pavel Grunt's avatar
Pavel Grunt committed
631
#if defined(GDK_WINDOWING_X11) && defined(HAVE_EGL)
Marc-André Lureau's avatar
Marc-André Lureau committed
632
633
    SpiceDisplay *display = SPICE_DISPLAY(user_data);

634
    if (GDK_IS_X11_DISPLAY(gdk_display_get_default()) &&
635
        spice_display_channel_get_gl_scanout(display->priv->display) != NULL) {
636
        spice_display_widget_gl_scanout(display);
637
    }
Marc-André Lureau's avatar
Marc-André Lureau committed
638
#endif
Marc-André Lureau's avatar
Marc-André Lureau committed
639
640
}

641
642
643
static void spice_display_init(SpiceDisplay *display)
{
    GtkWidget *widget = GTK_WIDGET(display);
Marc-André Lureau's avatar
Marc-André Lureau committed
644
    GtkWidget *area;
645
    SpiceDisplayPrivate *d;
646
    GtkTargetEntry targets = { "text/uri-list", 0, 0 };
647

648
    d = display->priv = spice_display_get_instance_private(display);
649
650
    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
651
    area = gtk_drawing_area_new();
652

Marc-André Lureau's avatar
Marc-André Lureau committed
653
654
655
656
    g_object_connect(area,
                     "signal::draw", draw_event, display,
                     "signal::realize", drawing_area_realize, display,
                     NULL);
657
658
    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
659

660
#if HAVE_EGL
Marc-André Lureau's avatar
Marc-André Lureau committed
661
662
663
664
665
666
667
    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);
668
    gtk_stack_add_named(d->stack, area, "gl-area");
669
#endif
670
671
    area = gtk_drawing_area_new();
    gtk_stack_add_named(d->stack, area, "gst-area");
672
673
674
675
    g_object_connect(area,
                     "signal::draw", gst_draw_event, display,
                     "signal::size-allocate", gst_size_allocate, display,
                     NULL);
676

677
678
679
    d->label = gtk_label_new(NULL);
    gtk_label_set_selectable(GTK_LABEL(d->label), true);
    gtk_stack_add_named(d->stack, d->label, "label");
680

681
    gtk_widget_show_all(widget);
Marc-André Lureau's avatar
Marc-André Lureau committed
682

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

686
687
688
    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);
689
    g_signal_connect(display, "size-allocate", G_CALLBACK(size_allocate), NULL);
690

691
692
693
694
695
696
697
    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 |
698
                          GDK_KEY_PRESS_MASK |
699
700
                          /* on Wayland, only smooth-scroll events are emitted */
                          GDK_SMOOTH_SCROLL_MASK |
701
                          GDK_SCROLL_MASK);
702
    gtk_widget_set_can_focus(widget, true);
703
704
    gtk_event_box_set_above_child(GTK_EVENT_BOX(widget), true);

705
    d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
706
    d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
707
708
709
710
711

#ifdef HAVE_WAYLAND_PROTOCOLS
    if GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(widget))
        spice_wayland_extensions_init(widget);
#endif
712
713
}

714
715
static void
spice_display_constructed(GObject *gobject)
716
717
718
719
720
721
{
    SpiceDisplay *display;
    SpiceDisplayPrivate *d;
    GList *list;
    GList *it;

722
    display = SPICE_DISPLAY(gobject);
723
    d = display->priv;
724
725
726
727

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

728
729
730
731
    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);
732
733
    list = spice_session_get_channels(d->session);
    for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
734
        if (SPICE_IS_MAIN_CHANNEL(it->data)) {
735
            channel_new(d->session, it->data, display);
736
737
738
739
740
            break;
        }
    }
    for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
        if (!SPICE_IS_MAIN_CHANNEL(it->data))
741
            channel_new(d->session, it->data, display);
742
743
744
    }
    g_list_free(list);

745
746
747
    spice_g_signal_connect_object(d->session, "notify::inhibit-keyboard-grab",
                                  G_CALLBACK(session_inhibit_keyboard_grab_changed),
                                  display, 0);
748
}
749

750
751
/**
 * spice_display_set_grab_keys:
752
753
 * @display: the display widget
 * @seq: (transfer none): key sequence
754
755
756
757
 *
 * Set the key combination to grab/ungrab the keyboard. The default is
 * "Control L + Alt L".
 **/
758
void spice_display_set_grab_keys(SpiceDisplay *display, SpiceGrabSequence *seq)
759
{
760
761
762
763
764
    SpiceDisplayPrivate *d;

    g_return_if_fail(SPICE_IS_DISPLAY(display));

    d = display->priv;
765
766
767
    g_return_if_fail(d != NULL);

    if (d->grabseq) {
768
        spice_grab_sequence_free(d->grabseq);
769
770
    }
    if (seq)
771
        d->grabseq = spice_grab_sequence_copy(seq);
772
    else
773
        d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
774
    g_free(d->activeseq);
775
776
777
    d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
}

778
#ifdef G_OS_WIN32
779
780
static LRESULT CALLBACK keyboard_hook_cb(int code, WPARAM wparam, LPARAM lparam)
{
781
    if  (win32_window && code == HC_ACTION && wparam != WM_KEYUP) {
782
783
784
785
786
        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);
787
            SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
788
789
790
791
792
793
794
795
796
797
798
        }
        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;
799
800
801
802
803
804
805
        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;
806
        default:
807
            SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
808
809
810
811
812
813
814
            return 1;
        }
    }
    return CallNextHookEx(NULL, code, wparam, lparam);
}
#endif

815
816
/**
 * spice_display_get_grab_keys:
817
 * @display: the display widget
818
 *
819
820
 * Finds the current grab key combination for the @display.
 *
821
 * Returns: (transfer none): the current grab key combination.
822
 **/
823
SpiceGrabSequence *spice_display_get_grab_keys(SpiceDisplay *display)
824
{
825
826
827
828
829
    SpiceDisplayPrivate *d;

    g_return_val_if_fail(SPICE_IS_DISPLAY(display), NULL);

    d = display->priv;
830
831
832
833
834
    g_return_val_if_fail(d != NULL, NULL);

    return d->grabseq;
}

835
836
837
838
839
840
841
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);
}

842
843
844
/* FIXME: gdk_keyboard_grab/ungrab() is deprecated */
G_GNUC_BEGIN_IGNORE_DEPRECATIONS

845
static void try_keyboard_grab(SpiceDisplay *display)
846
{
847
    GtkWidget *widget = GTK_WIDGET(display);
848
    SpiceDisplayPrivate *d = display->priv;
849
    GdkGrabStatus status;
850

851
852
    if (g_getenv("SPICE_NOGRAB"))
        return;
853
    if (d->disable_inputs)
854
855
        return;

856
857
    if (d->keyboard_grab_inhibit)
        return;
858
859
    if (!d->keyboard_grab_enable)
        return;
860
861
    if (d->keyboard_grab_active)
        return;
862
863
864
865
866
867
#ifdef G_OS_WIN32
    if (!d->keyboard_have_focus)
        return;
    if (!d->mouse_have_pointer)
        return;
#else
868
    if (!spice_gtk_session_get_keyboard_has_focus(d->gtk_session))
869
        return;
870
    if (!spice_gtk_session_get_mouse_has_pointer(d->gtk_session))
871
        return;
872
#endif
873
874
    if (d->keyboard_grab_released)
        return;
875

876
877
    g_return_if_fail(gtk_widget_is_focus(widget));

878
    DISPLAY_DEBUG(display, "grab keyboard");
879
    gtk_widget_grab_focus(widget);
880

881
#ifdef G_OS_WIN32
882
883
884
885
    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);
886
#endif
887
888
889
890
891
892
893
894
    status = gdk_seat_grab(spice_display_get_default_seat(display),
                           gtk_widget_get_window(widget),
                           GDK_SEAT_CAPABILITY_KEYBOARD,
                           FALSE,
                           NULL,
                           NULL,
                           NULL,
                           NULL);
895
    if (status != GDK_GRAB_SUCCESS) {
896
        g_warning("keyboard grab failed %u", status);
897
        d->keyboard_grab_active = false;
898
    } else {
899
        d->keyboard_grab_active = true;
900
901
        g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, true);
    }
902
903
}

904
static void ungrab_keyboard(SpiceDisplay *display)
905
{
906
907
908
909
910
911
912
913
    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.
     */
914
    if (GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(GTK_WIDGET(display)))) {
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
        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

941
942
943
944
945
946
947
    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
}

948
static void try_keyboard_ungrab(SpiceDisplay *display)
949
{
950
    SpiceDisplayPrivate *d = display->priv;
951
    GtkWidget *widget = GTK_WIDGET(display);
952
953
954
955

    if (!d->keyboard_grab_active)
        return;

956
    DISPLAY_DEBUG(display, "ungrab keyboard");
957
    ungrab_keyboard(display);
958
#ifdef G_OS_WIN32
959
960
961
962
963
    // do not use g_clear_pointer as Windows API have different linkage
    if (d->keyboard_hook) {
        UnhookWindowsHookEx(d->keyboard_hook);
        d->keyboard_hook = NULL;
    }
964
#endif
965
    d->keyboard_grab_active = false;
966
    g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, false);
967
}
968
969
G_GNUC_END_IGNORE_DEPRECATIONS

970

971
972
static void update_keyboard_grab(SpiceDisplay *display)
{
973
    SpiceDisplayPrivate *d = display->priv;
974
975
976
977
978
979
980
981
982

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

983
984
985
static void set_mouse_accel(SpiceDisplay *display, gboolean enabled)
{
#if defined GDK_WINDOWING_X11
986
    SpiceDisplayPrivate *d = display->priv;
987
988
    GdkWindow *w = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));

989
    if (!GDK_IS_X11_DISPLAY(gdk_window_get_display(w))) {
990
        DISPLAY_DEBUG(display, "FIXME: gtk backend is not X11");
991
992
993
994
        return;
    }

    Display *x_display = GDK_WINDOW_XDISPLAY(w);
995
996
997
998
999
1000
    if (enabled) {
        /* restore mouse acceleration */
        XChangePointerControl(x_display, True, True,
                              d->x11_accel_numerator, d->x11_accel_denominator, d->x11_threshold);
    } else {
        XGetPointerControl(x_display,
For faster browsing, not all history is shown. View entire blame