spice-widget.c 79 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
   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/>.
*/
18
#include "config.h"
19
20

#include <math.h>
21
#include <glib.h>
22
23
24
25
26

#if HAVE_X11_XKBLIB_H
#include <X11/XKBlib.h>
#include <gdk/gdkx.h>
#endif
27
28
29
30
#ifdef GDK_WINDOWING_X11
#include <X11/Xlib.h>
#include <gdk/gdkx.h>
#endif
31
#ifdef G_OS_WIN32
32
33
#include <windows.h>
#include <gdk/gdkwin32.h>
34
35
36
#ifndef MAPVK_VK_TO_VSC /* may be undefined in older mingw-headers */
#define MAPVK_VK_TO_VSC 0
#endif
37
#endif
38

39
#include "spice-widget.h"
40
#include "spice-widget-priv.h"
41
#include "spice-gtk-session-priv.h"
42
43
#include "vncdisplaykeymap.h"

44
#include "glib-compat.h"
45
#include "gtk-compat.h"
46

47
/* Some compatibility defines to let us build on both Gtk2 and Gtk3 */
48

49
50
51
52
53
54
55
56
/**
 * SECTION:spice-widget
 * @short_description: a GTK display widget
 * @title: Spice Display
 * @section_id:
 * @stability: Stable
 * @include: spice-widget.h
 *
57
58
59
60
61
62
63
64
65
66
67
68
69
70
 * 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).
71
72
 */

73
74
75
76
77
G_DEFINE_TYPE(SpiceDisplay, spice_display, GTK_TYPE_DRAWING_AREA)

/* Properties */
enum {
    PROP_0,
78
79
    PROP_SESSION,
    PROP_CHANNEL_ID,
80
81
82
    PROP_KEYBOARD_GRAB,
    PROP_MOUSE_GRAB,
    PROP_RESIZE_GUEST,
Gerd Hoffmann's avatar
Gerd Hoffmann committed
83
    PROP_AUTO_CLIPBOARD,
Marc-André Lureau's avatar
Marc-André Lureau committed
84
    PROP_SCALING,
85
    PROP_ONLY_DOWNSCALE,
86
    PROP_DISABLE_INPUTS,
87
    PROP_ZOOM_LEVEL,
88
    PROP_MONITOR_ID,
89
    PROP_KEYPRESS_DELAY,
90
    PROP_READY
91
92
93
94
};

/* Signals */
enum {
95
    SPICE_DISPLAY_MOUSE_GRAB,
96
    SPICE_DISPLAY_KEYBOARD_GRAB,
97
    SPICE_DISPLAY_GRAB_KEY_PRESSED,
98
99
100
101
102
    SPICE_DISPLAY_LAST_SIGNAL,
};

static guint signals[SPICE_DISPLAY_LAST_SIGNAL];

103
#ifdef G_OS_WIN32
104
static HWND win32_window = NULL;
105
106
#endif

107
static void update_keyboard_grab(SpiceDisplay *display);
108
109
static void try_keyboard_grab(SpiceDisplay *display);
static void try_keyboard_ungrab(SpiceDisplay *display);
110
static void update_mouse_grab(SpiceDisplay *display);
111
112
static void try_mouse_grab(SpiceDisplay *display);
static void try_mouse_ungrab(SpiceDisplay *display);
113
static void recalc_geometry(GtkWidget *widget);
Gerd Hoffmann's avatar
Gerd Hoffmann committed
114
115
static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data);
static void channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer data);
116
static void cursor_invalidate(SpiceDisplay *display);
117
static void update_area(SpiceDisplay *display, gint x, gint y, gint width, gint height);
118
static void release_keys(SpiceDisplay *display);
119
120
121
122
123
124
125
126
127

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

static void spice_display_get_property(GObject    *object,
                                       guint       prop_id,
                                       GValue     *value,
                                       GParamSpec *pspec)
{
    SpiceDisplay *display = SPICE_DISPLAY(object);
128
    SpiceDisplayPrivate *d = display->priv;
129
    gboolean boolean;
130
131

    switch (prop_id) {
132
133
134
135
136
137
    case PROP_SESSION:
        g_value_set_object(value, d->session);
        break;
    case PROP_CHANNEL_ID:
        g_value_set_int(value, d->channel_id);
        break;
138
139
140
    case PROP_MONITOR_ID:
        g_value_set_int(value, d->monitor_id);
        break;
141
142
    case PROP_KEYBOARD_GRAB:
        g_value_set_boolean(value, d->keyboard_grab_enable);
Marc-André Lureau's avatar
Marc-André Lureau committed
143
        break;
144
145
    case PROP_MOUSE_GRAB:
        g_value_set_boolean(value, d->mouse_grab_enable);
Marc-André Lureau's avatar
Marc-André Lureau committed
146
        break;
147
148
    case PROP_RESIZE_GUEST:
        g_value_set_boolean(value, d->resize_guest_enable);
Marc-André Lureau's avatar
Marc-André Lureau committed
149
        break;
Gerd Hoffmann's avatar
Gerd Hoffmann committed
150
    case PROP_AUTO_CLIPBOARD:
151
152
        g_object_get(d->gtk_session, "auto-clipboard", &boolean, NULL);
        g_value_set_boolean(value, boolean);
Marc-André Lureau's avatar
Marc-André Lureau committed
153
        break;
Marc-André Lureau's avatar
Marc-André Lureau committed
154
155
    case PROP_SCALING:
        g_value_set_boolean(value, d->allow_scaling);
156
157
158
159
        break;
    case PROP_ONLY_DOWNSCALE:
        g_value_set_boolean(value, d->only_downscale);
        break;
160
161
    case PROP_DISABLE_INPUTS:
        g_value_set_boolean(value, d->disable_inputs);
162
        break;
163
164
165
    case PROP_ZOOM_LEVEL:
        g_value_set_int(value, d->zoom_level);
        break;
166
167
168
    case PROP_READY:
        g_value_set_boolean(value, d->ready);
        break;
169
170
171
    case PROP_KEYPRESS_DELAY:
        g_value_set_uint(value, d->keypress_delay);
        break;
172
    default:
Marc-André Lureau's avatar
Marc-André Lureau committed
173
174
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
175
176
177
    }
}

178
179
static void scaling_updated(SpiceDisplay *display)
{
180
    SpiceDisplayPrivate *d = display->priv;
181
182
183
184
    GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(display));

    recalc_geometry(GTK_WIDGET(display));
    if (d->ximage && window) { /* if not yet shown */
185
        gtk_widget_queue_draw(GTK_WIDGET(display));
186
187
188
189
190
    }
}

static void update_size_request(SpiceDisplay *display)
{
191
    SpiceDisplayPrivate *d = display->priv;
192
193
194
195
196
197
    gint reqwidth, reqheight;

    if (d->resize_guest_enable) {
        reqwidth = 640;
        reqheight = 480;
    } else {
198
199
        reqwidth = d->area.width;
        reqheight = d->area.height;
200
201
202
203
204
205
    }

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

206
207
static void update_keyboard_focus(SpiceDisplay *display, gboolean state)
{
208
    SpiceDisplayPrivate *d = display->priv;
209
210
211
212
213
214
215
216
217
218
219
220
221

    d->keyboard_have_focus = state;

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

222
223
static void update_ready(SpiceDisplay *display)
{
224
    SpiceDisplayPrivate *d = display->priv;
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
    gboolean ready;

    ready = d->mark != 0 && d->monitor_ready;

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

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

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

260
static void update_monitor_area(SpiceDisplay *display)
261
{
262
    SpiceDisplayPrivate *d = display->priv;
263
    SpiceDisplayMonitorConfig *cfg, *c = NULL;
264
    GArray *monitors = NULL;
265
    int i;
266

267
268
269
    SPICE_DEBUG("update monitor area %d:%d", d->channel_id, d->monitor_id);
    if (d->monitor_id < 0)
        goto whole;
270
271

    g_object_get(d->display, "monitors", &monitors, NULL);
272
    for (i = 0; monitors != NULL && i < monitors->len; i++) {
273
        cfg = &g_array_index(monitors, SpiceDisplayMonitorConfig, i);
274
        if (cfg->id == d->monitor_id) {
275
276
           c = cfg;
           break;
277
278
279
        }
    }
    if (c == NULL) {
280
        SPICE_DEBUG("update monitor: no monitor %d", d->monitor_id);
281
        set_monitor_ready(display, false);
282
283
        if (spice_channel_test_capability(d->display, SPICE_DISPLAY_CAP_MONITORS_CONFIG)) {
            SPICE_DEBUG("waiting until MonitorsConfig is received");
284
            g_clear_pointer(&monitors, g_array_unref);
285
286
287
288
289
290
291
292
293
294
295
            return;
        }
        goto whole;
    }

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

296
    if (!d->resize_guest_enable)
297
298
        spice_main_update_display(d->main, get_display_id(display),
                                  c->x, c->y, c->width, c->height, FALSE);
299

300
    update_area(display, c->x, c->y, c->width, c->height);
301
    g_clear_pointer(&monitors, g_array_unref);
302
303
304
    return;

whole:
305
    g_clear_pointer(&monitors, g_array_unref);
306
307
    /* by display whole surface */
    update_area(display, 0, 0, d->width, d->height);
308
    set_monitor_ready(display, true);
309
310
}

311
312
313
314
315
316
static void spice_display_set_property(GObject      *object,
                                       guint         prop_id,
                                       const GValue *value,
                                       GParamSpec   *pspec)
{
    SpiceDisplay *display = SPICE_DISPLAY(object);
317
    SpiceDisplayPrivate *d = display->priv;
318
319

    switch (prop_id) {
320
    case PROP_SESSION:
321
322
        g_warn_if_fail(d->session == NULL);
        d->session = g_value_dup_object(value);
323
        d->gtk_session = spice_gtk_session_get(d->session);
324
325
326
        spice_g_signal_connect_object(d->gtk_session, "notify::pointer-grabbed",
                                      G_CALLBACK(cursor_invalidate), object,
                                      G_CONNECT_SWAPPED);
327
328
329
330
        break;
    case PROP_CHANNEL_ID:
        d->channel_id = g_value_get_int(value);
        break;
331
332
    case PROP_MONITOR_ID:
        d->monitor_id = g_value_get_int(value);
333
334
        if (d->display) /* if constructed */
            update_monitor_area(display);
335
        break;
336
337
    case PROP_KEYBOARD_GRAB:
        d->keyboard_grab_enable = g_value_get_boolean(value);
338
        update_keyboard_grab(display);
339
340
341
        break;
    case PROP_MOUSE_GRAB:
        d->mouse_grab_enable = g_value_get_boolean(value);
342
        update_mouse_grab(display);
343
344
345
        break;
    case PROP_RESIZE_GUEST:
        d->resize_guest_enable = g_value_get_boolean(value);
346
        update_size_request(display);
347
        break;
Marc-André Lureau's avatar
Marc-André Lureau committed
348
349
    case PROP_SCALING:
        d->allow_scaling = g_value_get_boolean(value);
350
        scaling_updated(display);
Marc-André Lureau's avatar
Marc-André Lureau committed
351
        break;
352
353
354
355
    case PROP_ONLY_DOWNSCALE:
        d->only_downscale = g_value_get_boolean(value);
        scaling_updated(display);
        break;
Gerd Hoffmann's avatar
Gerd Hoffmann committed
356
    case PROP_AUTO_CLIPBOARD:
357
358
        g_object_set(d->gtk_session, "auto-clipboard",
                     g_value_get_boolean(value), NULL);
Gerd Hoffmann's avatar
Gerd Hoffmann committed
359
        break;
360
361
362
363
364
365
    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;
366
367
368
369
    case PROP_ZOOM_LEVEL:
        d->zoom_level = g_value_get_int(value);
        scaling_updated(display);
        break;
370
371
372
    case PROP_KEYPRESS_DELAY:
        d->keypress_delay = g_value_get_uint(value);
        break;
373
374
375
376
377
378
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

379
380
381
382
383
384
385
386
387
static void gtk_session_property_changed(GObject    *gobject,
                                         GParamSpec *pspec,
                                         gpointer    user_data)
{
    SpiceDisplay *display = user_data;

    g_object_notify(G_OBJECT(display), g_param_spec_get_name(pspec));
}

388
389
390
391
392
static void session_inhibit_keyboard_grab_changed(GObject    *gobject,
                                                  GParamSpec *pspec,
                                                  gpointer    user_data)
{
    SpiceDisplay *display = user_data;
393
    SpiceDisplayPrivate *d = display->priv;
394
395
396

    g_object_get(d->session, "inhibit-keyboard-grab",
                 &d->keyboard_grab_inhibit, NULL);
397
    update_keyboard_grab(display);
398
    update_mouse_grab(display);
399
400
}

401
static void spice_display_dispose(GObject *obj)
402
{
Gerd Hoffmann's avatar
Gerd Hoffmann committed
403
    SpiceDisplay *display = SPICE_DISPLAY(obj);
404
    SpiceDisplayPrivate *d = display->priv;
Gerd Hoffmann's avatar
Gerd Hoffmann committed
405

406
    SPICE_DEBUG("spice display dispose");
Gerd Hoffmann's avatar
Gerd Hoffmann committed
407

408
409
410
    spicex_image_destroy(display);
    g_clear_object(&d->session);
    d->gtk_session = NULL;
411

412
413
414
415
416
    if (d->key_delayed_id) {
        g_source_remove(d->key_delayed_id);
        d->key_delayed_id = 0;
    }

417
    G_OBJECT_CLASS(spice_display_parent_class)->dispose(obj);
418
419
}

420
421
static void spice_display_finalize(GObject *obj)
{
422
    SpiceDisplay *display = SPICE_DISPLAY(obj);
423
    SpiceDisplayPrivate *d = display->priv;
424

425
    SPICE_DEBUG("Finalize spice display");
426
427

    if (d->grabseq) {
428
        spice_grab_sequence_free(d->grabseq);
429
430
        d->grabseq = NULL;
    }
431
432
    g_free(d->activeseq);
    d->activeseq = NULL;
433

434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
    if (d->show_cursor) {
        gdk_cursor_unref(d->show_cursor);
        d->show_cursor = NULL;
    }

    if (d->mouse_cursor) {
        gdk_cursor_unref(d->mouse_cursor);
        d->mouse_cursor = NULL;
    }

    if (d->mouse_pixbuf) {
        g_object_unref(d->mouse_pixbuf);
        d->mouse_pixbuf = NULL;
    }

449
450
451
    G_OBJECT_CLASS(spice_display_parent_class)->finalize(obj);
}

452
453
454
455
456
457
458
459
static GdkCursor* get_blank_cursor(void)
{
    if (g_getenv("SPICE_DEBUG_CURSOR"))
        return gdk_cursor_new(GDK_DOT);

    return gdk_cursor_new(GDK_BLANK_CURSOR);
}

Marc-André Lureau's avatar
Marc-André Lureau committed
460
461
462
static gboolean grab_broken(SpiceDisplay *self, GdkEventGrabBroken *event,
                            gpointer user_data G_GNUC_UNUSED)
{
463
464
    SPICE_DEBUG("%s (implicit: %d, keyboard: %d)", __FUNCTION__,
                event->implicit, event->keyboard);
Marc-André Lureau's avatar
Marc-André Lureau committed
465
466

    if (event->keyboard) {
467
        try_keyboard_ungrab(self);
468
        release_keys(self);
Marc-André Lureau's avatar
Marc-André Lureau committed
469
470
    }

471
472
473
474
475
    /* 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
476
477
478
    return false;
}

479
480
481
482
483
484
485
486
487
static void drag_data_received_callback(SpiceDisplay *self,
                                        GdkDragContext *drag_context,
                                        gint x,
                                        gint y,
                                        GtkSelectionData *data,
                                        guint info,
                                        guint time,
                                        gpointer *user_data)
{
488
    const guchar *buf;
489
490
    gchar **file_urls;
    int n_files;
491
    SpiceDisplayPrivate *d = self->priv;
492
493
494
495
496
497
498
    int i = 0;
    GFile **files;

    /* We get a buf like:
     * file:///root/a.txt\r\nfile:///root/b.txt\r\n
     */
    SPICE_DEBUG("%s: drag a file", __FUNCTION__);
499
    buf = gtk_selection_data_get_data(data);
500
501
    g_return_if_fail(buf != NULL);

502
    file_urls = g_uri_list_extract_uris((const gchar*)buf);
503
    n_files = g_strv_length(file_urls);
504
    files = g_new0(GFile*, n_files + 1);
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
    for (i = 0; i < n_files; i++) {
        files[i] = g_file_new_for_uri(file_urls[i]);
    }
    g_strfreev(file_urls);

    spice_main_file_copy_async(d->main, files, 0, NULL, NULL,
                               NULL, NULL, NULL);
    for (i = 0; i < n_files; i++) {
        g_object_unref(files[i]);
    }
    g_free(files);

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

520
521
522
523
524
525
526
527
static void grab_notify(SpiceDisplay *display, gboolean was_grabbed)
{
    SPICE_DEBUG("grab notify %d", was_grabbed);

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

528
529
530
static void spice_display_init(SpiceDisplay *display)
{
    GtkWidget *widget = GTK_WIDGET(display);
531
    SpiceDisplayPrivate *d;
532
    GtkTargetEntry targets = { "text/uri-list", 0, 0 };
533
534
535

    d = display->priv = SPICE_DISPLAY_GET_PRIVATE(display);

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

539
540
541
    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);
542

543
544
545
546
547
548
549
550
    gtk_widget_add_events(widget,
                          GDK_STRUCTURE_MASK |
                          GDK_POINTER_MOTION_MASK |
                          GDK_BUTTON_PRESS_MASK |
                          GDK_BUTTON_RELEASE_MASK |
                          GDK_BUTTON_MOTION_MASK |
                          GDK_ENTER_NOTIFY_MASK |
                          GDK_LEAVE_NOTIFY_MASK |
551
552
                          GDK_KEY_PRESS_MASK |
                          GDK_SCROLL_MASK);
553
554
555
#ifdef WITH_X11
    gtk_widget_set_double_buffered(widget, false);
#else
556
    gtk_widget_set_double_buffered(widget, true);
557
#endif
558
    gtk_widget_set_can_focus(widget, true);
559
    gtk_widget_set_has_window(widget, true);
560
    d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
561
    d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
562

563
    d->mouse_cursor = get_blank_cursor();
564
565
566
    d->have_mitshm = true;
}

567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
static GObject *
spice_display_constructor(GType                  gtype,
                          guint                  n_properties,
                          GObjectConstructParam *properties)
{
    GObject *obj;
    SpiceDisplay *display;
    SpiceDisplayPrivate *d;
    GList *list;
    GList *it;

    {
        /* Always chain up to the parent constructor */
        GObjectClass *parent_class;
        parent_class = G_OBJECT_CLASS(spice_display_parent_class);
        obj = parent_class->constructor(gtype, n_properties, properties);
    }

    display = SPICE_DISPLAY(obj);
586
    d = display->priv;
587
588
589
590

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

591
592
593
594
    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);
595
596
    list = spice_session_get_channels(d->session);
    for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
597
598
599
600
601
602
603
604
        if (SPICE_IS_MAIN_CHANNEL(it->data)) {
            channel_new(d->session, it->data, (gpointer*)display);
            break;
        }
    }
    for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
        if (!SPICE_IS_MAIN_CHANNEL(it->data))
            channel_new(d->session, it->data, (gpointer*)display);
605
606
607
    }
    g_list_free(list);

608
609
    spice_g_signal_connect_object(d->gtk_session, "notify::auto-clipboard",
                                  G_CALLBACK(gtk_session_property_changed), display, 0);
610

611
612
613
    spice_g_signal_connect_object(d->session, "notify::inhibit-keyboard-grab",
                                  G_CALLBACK(session_inhibit_keyboard_grab_changed),
                                  display, 0);
614

615
616
    return obj;
}
617

618
619
/**
 * spice_display_set_grab_keys:
620
621
 * @display: the display widget
 * @seq: (transfer none): key sequence
622
623
624
625
 *
 * Set the key combination to grab/ungrab the keyboard. The default is
 * "Control L + Alt L".
 **/
626
void spice_display_set_grab_keys(SpiceDisplay *display, SpiceGrabSequence *seq)
627
{
628
629
630
631
632
    SpiceDisplayPrivate *d;

    g_return_if_fail(SPICE_IS_DISPLAY(display));

    d = display->priv;
633
634
635
    g_return_if_fail(d != NULL);

    if (d->grabseq) {
636
        spice_grab_sequence_free(d->grabseq);
637
638
    }
    if (seq)
639
        d->grabseq = spice_grab_sequence_copy(seq);
640
    else
641
        d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
642
    g_free(d->activeseq);
643
644
645
    d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
}

646
#ifdef G_OS_WIN32
647
648
static LRESULT CALLBACK keyboard_hook_cb(int code, WPARAM wparam, LPARAM lparam)
{
649
    if  (win32_window && code == HC_ACTION && wparam != WM_KEYUP) {
650
651
652
653
654
        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);
655
            SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
656
657
658
659
660
661
662
663
664
665
666
        }
        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;
667
668
669
670
671
672
673
        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;
674
        default:
675
            SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
676
677
678
679
680
681
682
            return 1;
        }
    }
    return CallNextHookEx(NULL, code, wparam, lparam);
}
#endif

683
684
/**
 * spice_display_get_grab_keys:
685
 * @display: the display widget
686
 *
687
 * Returns: (transfer none): the current grab key combination.
688
 **/
689
SpiceGrabSequence *spice_display_get_grab_keys(SpiceDisplay *display)
690
{
691
692
693
694
695
    SpiceDisplayPrivate *d;

    g_return_val_if_fail(SPICE_IS_DISPLAY(display), NULL);

    d = display->priv;
696
697
698
699
700
    g_return_val_if_fail(d != NULL, NULL);

    return d->grabseq;
}

701
static void try_keyboard_grab(SpiceDisplay *display)
702
{
703
    GtkWidget *widget = GTK_WIDGET(display);
704
    SpiceDisplayPrivate *d = display->priv;
705
    GdkGrabStatus status;
706

707
708
    if (g_getenv("SPICE_NOGRAB"))
        return;
709
    if (d->disable_inputs)
710
711
        return;

712
713
    if (d->keyboard_grab_inhibit)
        return;
714
715
    if (!d->keyboard_grab_enable)
        return;
716
717
    if (d->keyboard_grab_active)
        return;
718
719
720
721
    if (!d->keyboard_have_focus)
        return;
    if (!d->mouse_have_pointer)
        return;
722
723
    if (d->keyboard_grab_released)
        return;
724

725
726
    g_return_if_fail(gtk_widget_is_focus(widget));

727
    SPICE_DEBUG("grab keyboard");
728
    gtk_widget_grab_focus(widget);
729

730
#ifdef G_OS_WIN32
731
732
733
734
    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);
735
#endif
736
737
738
739
740
    status = gdk_keyboard_grab(gtk_widget_get_window(widget), FALSE,
                               GDK_CURRENT_TIME);
    if (status != GDK_GRAB_SUCCESS) {
        g_warning("keyboard grab failed %d", status);
        d->keyboard_grab_active = false;
741
    } else {
742
        d->keyboard_grab_active = true;
743
744
        g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, true);
    }
745
746
}

747
static void try_keyboard_ungrab(SpiceDisplay *display)
748
{
749
    SpiceDisplayPrivate *d = display->priv;
750
    GtkWidget *widget = GTK_WIDGET(display);
751
752
753
754

    if (!d->keyboard_grab_active)
        return;

755
    SPICE_DEBUG("ungrab keyboard");
756
    gdk_keyboard_ungrab(GDK_CURRENT_TIME);
757
#ifdef G_OS_WIN32
758
759
760
761
    if (d->keyboard_hook != NULL) {
        UnhookWindowsHookEx(d->keyboard_hook);
        d->keyboard_hook = NULL;
    }
762
#endif
763
    d->keyboard_grab_active = false;
764
    g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, false);
765
766
}

767
768
static void update_keyboard_grab(SpiceDisplay *display)
{
769
    SpiceDisplayPrivate *d = display->priv;
770
771
772
773
774
775
776
777
778

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

779
780
static void set_mouse_accel(SpiceDisplay *display, gboolean enabled)
{
781
    SpiceDisplayPrivate *d = display->priv;
782
783
784
785

#if defined GDK_WINDOWING_X11
    GdkWindow *w = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));

786
787
788
789
790
791
    if (!GDK_IS_X11_DISPLAY(gdk_window_get_display(w))) {
        SPICE_DEBUG("FIXME: gtk backend is not X11");
        return;
    }

    Display *x_display = GDK_WINDOW_XDISPLAY(w);
792
793
794
795
796
797
798
799
800
801
802
803
    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,
                           &d->x11_accel_numerator, &d->x11_accel_denominator, &d->x11_threshold);
        /* set mouse acceleration to default */
        XChangePointerControl(x_display, True, True, -1, -1, -1);
        SPICE_DEBUG("disabled X11 mouse motion %d %d %d",
                    d->x11_accel_numerator, d->x11_accel_denominator, d->x11_threshold);
    }
804
805
806
#elif defined GDK_WINDOWING_WIN32
    if (enabled) {
        g_return_if_fail(SystemParametersInfo(SPI_SETMOUSE, 0, &d->win_mouse, 0));
807
        g_return_if_fail(SystemParametersInfo(SPI_SETMOUSESPEED, 0, (PVOID)(INT_PTR)d->win_mouse_speed, 0));
808
809
810
811
812
813
814
    } else {
        int accel[3] = { 0, 0, 0 }; // disabled
        g_return_if_fail(SystemParametersInfo(SPI_GETMOUSE, 0, &d->win_mouse, 0));
        g_return_if_fail(SystemParametersInfo(SPI_GETMOUSESPEED, 0, &d->win_mouse_speed, 0));
        g_return_if_fail(SystemParametersInfo(SPI_SETMOUSE, 0, &accel, SPIF_SENDCHANGE));
        g_return_if_fail(SystemParametersInfo(SPI_SETMOUSESPEED, 0, (PVOID)10, SPIF_SENDCHANGE)); // default
    }
815
816
817
818
819
#else
    g_warning("Mouse acceleration code missing for your platform");
#endif
}

820
#ifdef G_OS_WIN32
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
static gboolean win32_clip_cursor(void)
{
    RECT window, workarea, rect;
    HMONITOR monitor;
    MONITORINFO mi = { 0, };

    g_return_val_if_fail(win32_window != NULL, FALSE);

    if (!GetWindowRect(win32_window, &window))
        goto error;

    monitor = MonitorFromRect(&window, MONITOR_DEFAULTTONEAREST);
    g_return_val_if_fail(monitor != NULL, false);

    mi.cbSize = sizeof(mi);
    if (!GetMonitorInfo(monitor, &mi))
        goto error;
    workarea = mi.rcWork;

    if (!IntersectRect(&rect, &window, &workarea)) {
        g_critical("error clipping cursor");
        return false;
    }

    SPICE_DEBUG("clip rect %ld %ld %ld %ld\n",
                rect.left, rect.right, rect.top, rect.bottom);

    if (!ClipCursor(&rect))
        goto error;

    return true;

error:
    {
        DWORD errval  = GetLastError();
        gchar *errstr = g_win32_error_message(errval);
        g_warning("failed to clip cursor (%ld) %s", errval, errstr);
    }

    return false;
}
#endif

864
static GdkGrabStatus do_pointer_grab(SpiceDisplay *display)
Gerd Hoffmann's avatar
Gerd Hoffmann committed
865
{
866
    SpiceDisplayPrivate *d = display->priv;
867
    GdkWindow *window = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));
868
    GdkGrabStatus status = GDK_GRAB_BROKEN;
869
    GdkCursor *blank = get_blank_cursor();
Gerd Hoffmann's avatar
Gerd Hoffmann committed
870

871
    if (!gtk_widget_get_realized(GTK_WIDGET(display)))
872
873
        goto end;

874
#ifdef G_OS_WIN32
875
876
877
    if (!win32_clip_cursor())
        goto end;
#endif
878

879
    try_keyboard_grab(display);
880
881
882
883
884
885
886
887
888
    /*
     * from gtk-vnc:
     * For relative mouse to work correctly when grabbed we need to
     * allow the pointer to move anywhere on the local desktop, so
     * use NULL for the 'confine_to' argument. Furthermore we need
     * the coords to be reported to our VNC window, regardless of
     * what window the pointer is actally over, so use 'FALSE' for
     * 'owner_events' parameter
     */
889
    status = gdk_pointer_grab(window, FALSE,
Gerd Hoffmann's avatar
Gerd Hoffmann committed
890
891
892
                     GDK_POINTER_MOTION_MASK |
                     GDK_BUTTON_PRESS_MASK |
                     GDK_BUTTON_RELEASE_MASK |
893
894
                     GDK_BUTTON_MOTION_MASK |
                     GDK_SCROLL_MASK,
895
896
                     NULL,
                     blank,
Gerd Hoffmann's avatar
Gerd Hoffmann committed
897
                     GDK_CURRENT_TIME);
898
899
    if (status != GDK_GRAB_SUCCESS) {
        d->mouse_grab_active = false;
900
        g_warning("pointer grab failed %d", status);
901
902
903
    } else {
        d->mouse_grab_active = true;
        g_signal_emit(display, signals[SPICE_DISPLAY_MOUSE_GRAB], 0, true);
904
        spice_gtk_session_set_pointer_grabbed(d->gtk_session, true);
905
        set_mouse_accel(display, FALSE);
906
    }
907

908
end:
909
    gdk_cursor_unref(blank);
910
    return status;
Gerd Hoffmann's avatar
Gerd Hoffmann committed
911
912
}

Gerd Hoffmann's avatar
Gerd Hoffmann committed
913
914
static void update_mouse_pointer(SpiceDisplay *display)
{
915
    SpiceDisplayPrivate *d = display->priv;
916
    GdkWindow *window = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));
Gerd Hoffmann's avatar
Gerd Hoffmann committed
917
918
919
920
921
922

    if (!window)
        return;

    switch (d->mouse_mode) {
    case SPICE_MOUSE_MODE_CLIENT:
923
924
        if (gdk_window_get_cursor(window) != d->mouse_cursor)
            gdk_window_set_cursor(window, d->mouse_cursor);
Gerd Hoffmann's avatar
Gerd Hoffmann committed
925
926
        break;
    case SPICE_MOUSE_MODE_SERVER:
927
928
        if (gdk_window_get_cursor(window) != NULL)
            gdk_window_set_cursor(window, NULL);
Gerd Hoffmann's avatar
Gerd Hoffmann committed
929
930
        break;
    default:
931
        g_warn_if_reached();
Gerd Hoffmann's avatar
Gerd Hoffmann committed
932
933
934
935
        break;
    }
}

936
static void try_mouse_grab(SpiceDisplay *display)
937
{
938
    SpiceDisplayPrivate *d = display->priv;
939

940
941
    if (g_getenv("SPICE_NOGRAB"))
        return;
942
943
    if (d->disable_inputs)
        return;
944

945
946
947
948
949
    if (!d->mouse_have_pointer)
        return;
    if (!d->keyboard_have_focus)
        return;

950
951
952
953
954
955
956
    if (!d->mouse_grab_enable)
        return;
    if (d->mouse_mode != SPICE_MOUSE_MODE_SERVER)
        return;
    if (d->mouse_grab_active)
        return;

957
958
959
    if (do_pointer_grab(display) != GDK_GRAB_SUCCESS)
        return;

960
961
962
963
    d->mouse_last_x = -1;
    d->mouse_last_y = -1;
}

964
static void mouse_wrap(SpiceDisplay *display, GdkEventMotion *motion)
965
{
966
    SpiceDisplayPrivate *d = display->priv;
967
    gint xr, yr;
968

969
#ifdef G_OS_WIN32
970
971
972
973
974
975
976
977
978
979
980
981
    RECT clip;
    g_return_if_fail(GetClipCursor(&clip));
    xr = clip.left + (clip.right - clip.left) / 2;
    yr = clip.top + (clip.bottom - clip.top) / 2;
    /* the clip rectangle has no offset, so we can't use gdk_wrap_pointer */
    SetCursorPos(xr, yr);
    d->mouse_last_x = -1;
    d->mouse_last_y = -1;
#else
    GdkScreen *screen = gtk_widget_get_screen(GTK_WIDGET(display));
    xr = gdk_screen_get_width(screen) / 2;
    yr = gdk_screen_get_height(screen) / 2;
982
983

    if (xr != (gint)motion->x_root || yr != (gint)motion->y_root) {
984
985
986
        /* FIXME: we try our best to ignore that next pointer move event.. */
        gdk_display_sync(gdk_screen_get_display(screen));

987
        gdk_display_warp_pointer(gtk_widget_get_display(GTK_WIDGET(display)),
988
                                 screen, xr, yr);
989
990
991
        d->mouse_last_x = -1;
        d->mouse_last_y = -1;
    }
992
993
#endif

994
995
}

996
static void try_mouse_ungrab(SpiceDisplay *display)
997
{
998
    SpiceDisplayPrivate *d = display->priv;
999
1000
    double s;
    int x, y;
For faster browsing, not all history is shown. View entire blame