]> asedeno.scripts.mit.edu Git - PuTTY.git/blob - unix/gtkask.c
GTK3 port: use gdk_device_grab() in gtkask.c.
[PuTTY.git] / unix / gtkask.c
1 /*
2  * GTK implementation of a GUI password/passphrase prompt.
3  */
4
5 #include <assert.h>
6 #include <time.h>
7 #include <stdlib.h>
8
9 #include <unistd.h>
10
11 #include <gtk/gtk.h>
12 #include <gdk/gdk.h>
13 #if !GTK_CHECK_VERSION(3,0,0)
14 #include <gdk/gdkkeysyms.h>
15 #endif
16
17 #include "gtkfont.h"
18 #include "gtkcompat.h"
19
20 #include "misc.h"
21
22 #define N_DRAWING_AREAS 3
23
24 struct drawing_area_ctx {
25     GtkWidget *area;
26 #ifndef DRAW_DEFAULT_CAIRO
27     GdkColor *cols;
28 #endif
29     int width, height, current;
30 };
31
32 struct askpass_ctx {
33     GtkWidget *dialog, *promptlabel;
34     struct drawing_area_ctx drawingareas[N_DRAWING_AREAS];
35     int active_area;
36 #if GTK_CHECK_VERSION(2,0,0)
37     GtkIMContext *imc;
38 #endif
39 #ifndef DRAW_DEFAULT_CAIRO
40     GdkColormap *colmap;
41     GdkColor cols[2];
42 #endif
43     char *passphrase;
44     int passlen, passsize;
45 #if GTK_CHECK_VERSION(3,0,0)
46     GdkDevice *keyboard;               /* for gdk_device_grab */
47 #endif
48 };
49
50 static void visually_acknowledge_keypress(struct askpass_ctx *ctx)
51 {
52     int new_active;
53     new_active = rand() % (N_DRAWING_AREAS - 1);
54     if (new_active >= ctx->active_area)
55         new_active++;
56     ctx->drawingareas[ctx->active_area].current = 0;
57     gtk_widget_queue_draw(ctx->drawingareas[ctx->active_area].area);
58     ctx->drawingareas[new_active].current = 1;
59     gtk_widget_queue_draw(ctx->drawingareas[new_active].area);
60     ctx->active_area = new_active;
61 }
62
63 static int last_char_len(struct askpass_ctx *ctx)
64 {
65     /*
66      * GTK always encodes in UTF-8, so we can do this in a fixed way.
67      */
68     int i;
69     assert(ctx->passlen > 0);
70     i = ctx->passlen - 1;
71     while ((unsigned)((unsigned char)ctx->passphrase[i] - 0x80) < 0x40) {
72         if (i == 0)
73             break;
74         i--;
75     }
76     return ctx->passlen - i;
77 }
78
79 static void add_text_to_passphrase(struct askpass_ctx *ctx, gchar *str)
80 {
81     int len = strlen(str);
82     if (ctx->passlen + len >= ctx->passsize) {
83         /* Take some care with buffer expansion, because there are
84          * pieces of passphrase in the old buffer so we should ensure
85          * realloc doesn't leave a copy lying around in the address
86          * space. */
87         int oldsize = ctx->passsize;
88         char *newbuf;
89
90         ctx->passsize = (ctx->passlen + len) * 5 / 4 + 1024;
91         newbuf = snewn(ctx->passsize, char);
92         memcpy(newbuf, ctx->passphrase, oldsize);
93         smemclr(ctx->passphrase, oldsize);
94         sfree(ctx->passphrase);
95         ctx->passphrase = newbuf;
96     }
97     strcpy(ctx->passphrase + ctx->passlen, str);
98     ctx->passlen += len;
99     visually_acknowledge_keypress(ctx);
100 }
101
102 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
103 {
104     struct askpass_ctx *ctx = (struct askpass_ctx *)data;
105
106     if (event->keyval == GDK_KEY_Return &&
107         event->type == GDK_KEY_PRESS) {
108         gtk_main_quit();
109     } else if (event->keyval == GDK_KEY_Escape &&
110                event->type == GDK_KEY_PRESS) {
111         smemclr(ctx->passphrase, ctx->passsize);
112         ctx->passphrase = NULL;
113         gtk_main_quit();
114     } else {
115 #if GTK_CHECK_VERSION(2,0,0)
116         if (gtk_im_context_filter_keypress(ctx->imc, event))
117             return TRUE;
118 #endif
119
120         if (event->type == GDK_KEY_PRESS) {
121             if (!strcmp(event->string, "\x15")) {
122                 /* Ctrl-U. Wipe out the whole line */
123                 ctx->passlen = 0;
124                 visually_acknowledge_keypress(ctx);
125             } else if (!strcmp(event->string, "\x17")) {
126                 /* Ctrl-W. Delete back to the last space->nonspace
127                  * boundary. We interpret 'space' in a really simple
128                  * way (mimicking terminal drivers), and don't attempt
129                  * to second-guess exciting Unicode space
130                  * characters. */
131                 while (ctx->passlen > 0) {
132                     char deleted, prior;
133                     ctx->passlen -= last_char_len(ctx);
134                     deleted = ctx->passphrase[ctx->passlen];
135                     prior = (ctx->passlen == 0 ? ' ' :
136                              ctx->passphrase[ctx->passlen-1]);
137                     if (!g_ascii_isspace(deleted) && g_ascii_isspace(prior))
138                         break;
139                 }
140                 visually_acknowledge_keypress(ctx);
141             } else if (event->keyval == GDK_KEY_BackSpace) {
142                 /* Backspace. Delete one character. */
143                 if (ctx->passlen > 0)
144                     ctx->passlen -= last_char_len(ctx);
145                 visually_acknowledge_keypress(ctx);
146 #if !GTK_CHECK_VERSION(2,0,0)
147             } else if (event->string[0]) {
148                 add_text_to_passphrase(ctx, event->string);
149 #endif
150             }
151         }
152     }
153     return TRUE;
154 }
155
156 #if GTK_CHECK_VERSION(2,0,0)
157 static void input_method_commit_event(GtkIMContext *imc, gchar *str,
158                                       gpointer data)
159 {
160     struct askpass_ctx *ctx = (struct askpass_ctx *)data;
161     add_text_to_passphrase(ctx, str);
162 }
163 #endif
164
165 static gint configure_area(GtkWidget *widget, GdkEventConfigure *event,
166                            gpointer data)
167 {
168     struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
169     ctx->width = event->width;
170     ctx->height = event->height;
171     gtk_widget_queue_draw(widget);
172     return TRUE;
173 }
174
175 #ifdef DRAW_DEFAULT_CAIRO
176 static void askpass_redraw_cairo(cairo_t *cr, struct drawing_area_ctx *ctx)
177 {
178     cairo_set_source_rgb(cr, 1-ctx->current, 1-ctx->current, 1-ctx->current);
179     cairo_paint(cr);
180 }
181 #else
182 static void askpass_redraw_gdk(GdkWindow *win, struct drawing_area_ctx *ctx)
183 {
184     GdkGC *gc = gdk_gc_new(win);
185     gdk_gc_set_foreground(gc, &ctx->cols[ctx->current]);
186     gdk_draw_rectangle(win, gc, TRUE, 0, 0, ctx->width, ctx->height);
187     gdk_gc_unref(gc);
188 }
189 #endif
190
191 #if GTK_CHECK_VERSION(3,0,0)
192 static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
193 {
194     struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
195     askpass_redraw_cairo(cr, ctx);
196     return TRUE;
197 }
198 #else
199 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
200                         gpointer data)
201 {
202     struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
203
204 #ifdef DRAW_DEFAULT_CAIRO
205     cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(ctx->area));
206     askpass_redraw_cairo(cr, ctx);
207     cairo_destroy(cr);
208 #else
209     askpass_redraw_gdk(gtk_widget_get_window(ctx->area), ctx);
210 #endif
211
212     return TRUE;
213 }
214 #endif
215
216 static int try_grab_keyboard(struct askpass_ctx *ctx)
217 {
218     int ret;
219
220 #if GTK_CHECK_VERSION(3,0,0)
221     /*
222      * Grabbing the keyboard is quite complicated in GTK 3.
223      */
224     GdkDeviceManager *dm;
225     GdkDevice *pointer, *keyboard;
226
227     dm = gdk_display_get_device_manager
228         (gtk_widget_get_display(ctx->dialog));
229     if (!dm)
230         return FALSE;
231
232     pointer = gdk_device_manager_get_client_pointer(dm);
233     if (!pointer)
234         return FALSE;
235     keyboard = gdk_device_get_associated_device(pointer);
236     if (!keyboard)
237         return FALSE;
238     if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
239         return FALSE;
240
241     ctx->keyboard = keyboard;
242     ret = gdk_device_grab(ctx->keyboard,
243                           gtk_widget_get_window(ctx->dialog),
244                           GDK_OWNERSHIP_NONE,
245                           TRUE,
246                           GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
247                           NULL,
248                           GDK_CURRENT_TIME);
249 #else
250     /*
251      * It's much simpler in GTK 1 and 2!
252      */
253     ret = gdk_keyboard_grab(gtk_widget_get_window(ctx->dialog),
254                             FALSE, GDK_CURRENT_TIME);
255 #endif
256
257     return ret == GDK_GRAB_SUCCESS;
258 }
259
260 typedef int (try_grab_fn_t)(struct askpass_ctx *ctx);
261
262 static int repeatedly_try_grab(struct askpass_ctx *ctx, try_grab_fn_t fn)
263 {
264     /*
265      * Repeatedly try to grab some aspect of the X server. We have to
266      * do this rather than just trying once, because there is at least
267      * one important situation in which the grab may fail the first
268      * time: any user who is launching an add-key operation off some
269      * kind of window manager hotkey will almost by definition be
270      * running this script with a keyboard grab already active, namely
271      * the one-key grab that the WM (or whatever) uses to detect
272      * presses of the hotkey. So at the very least we have to give the
273      * user time to release that key.
274      */
275     const useconds_t ms_limit = 5*1000000;  /* try for 5 seconds */
276     const useconds_t ms_step = 1000000/8;   /* at 1/8 second intervals */
277     useconds_t ms;
278
279     for (ms = 0; ms < ms_limit; ms += ms_step) {
280         if (fn(ctx))
281             return TRUE;
282         usleep(ms_step);
283     }
284     return FALSE;
285 }
286
287 static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
288                                      const char *window_title,
289                                      const char *prompt_text)
290 {
291     int i;
292
293     ctx->passlen = 0;
294     ctx->passsize = 2048;
295     ctx->passphrase = snewn(ctx->passsize, char);
296
297     /*
298      * Create widgets.
299      */
300     ctx->dialog = gtk_dialog_new();
301     gtk_window_set_title(GTK_WINDOW(ctx->dialog), window_title);
302     ctx->promptlabel = gtk_label_new(prompt_text);
303     gtk_label_set_line_wrap(GTK_LABEL(ctx->promptlabel), TRUE);
304     gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area
305                                     (GTK_DIALOG(ctx->dialog))),
306                       ctx->promptlabel);
307 #if GTK_CHECK_VERSION(2,0,0)
308     ctx->imc = gtk_im_multicontext_new();
309 #endif
310 #ifndef DRAW_DEFAULT_CAIRO
311     {
312         gboolean success[2];
313         ctx->colmap = gdk_colormap_get_system();
314         ctx->cols[0].red = ctx->cols[0].green = ctx->cols[0].blue = 0xFFFF;
315         ctx->cols[1].red = ctx->cols[1].green = ctx->cols[1].blue = 0;
316         gdk_colormap_alloc_colors(ctx->colmap, ctx->cols, 2,
317                                   FALSE, TRUE, success);
318         if (!success[0] | !success[1])
319             return "unable to allocate colours";
320     }
321 #endif
322     for (i = 0; i < N_DRAWING_AREAS; i++) {
323         ctx->drawingareas[i].area = gtk_drawing_area_new();
324 #ifndef DRAW_DEFAULT_CAIRO
325         ctx->drawingareas[i].cols = ctx->cols;
326 #endif
327         ctx->drawingareas[i].current = 0;
328         ctx->drawingareas[i].width = ctx->drawingareas[i].height = 0;
329         /* It would be nice to choose this size in some more
330          * context-sensitive way, like measuring the size of some
331          * piece of template text. */
332         gtk_widget_set_size_request(ctx->drawingareas[i].area, 32, 32);
333         gtk_container_add(GTK_CONTAINER(gtk_dialog_get_action_area
334                                         (GTK_DIALOG(ctx->dialog))),
335                           ctx->drawingareas[i].area);
336         g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
337                          "configure_event",
338                          G_CALLBACK(configure_area),
339                          &ctx->drawingareas[i]);
340 #if GTK_CHECK_VERSION(3,0,0)
341         g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
342                          "draw",
343                          G_CALLBACK(draw_area),
344                          &ctx->drawingareas[i]);
345 #else
346         g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
347                          "expose_event",
348                          G_CALLBACK(expose_area),
349                          &ctx->drawingareas[i]);
350 #endif
351         gtk_widget_show(ctx->drawingareas[i].area);
352     }
353     ctx->active_area = rand() % N_DRAWING_AREAS;
354     ctx->drawingareas[ctx->active_area].current = 1;
355
356     /*
357      * Arrange to receive key events. We don't really need to worry
358      * from a UI perspective about which widget gets the events, as
359      * long as we know which it is so we can catch them. So we'll pick
360      * the prompt label at random, and we'll use gtk_grab_add to
361      * ensure key events go to it.
362      */
363     gtk_widget_set_sensitive(ctx->promptlabel, TRUE);
364
365 #if GTK_CHECK_VERSION(2,0,0)
366     gtk_window_set_keep_above(GTK_WINDOW(ctx->dialog), TRUE);
367 #endif
368
369     /*
370      * Actually show the window, and wait for it to be shown.
371      */
372     gtk_widget_show_now(ctx->dialog);
373
374     /*
375      * Now that the window is displayed, make it grab the input focus.
376      */
377     gtk_grab_add(ctx->promptlabel);
378     if (!repeatedly_try_grab(ctx, try_grab_keyboard))
379         return "unable to grab keyboard";
380
381     /*
382      * And now that we've got the keyboard grab, connect up our
383      * keyboard handlers, and display the prompt.
384      */
385 #if GTK_CHECK_VERSION(2,0,0)
386     g_signal_connect(G_OBJECT(ctx->imc), "commit",
387                      G_CALLBACK(input_method_commit_event), ctx);
388 #endif
389     g_signal_connect(G_OBJECT(ctx->promptlabel), "key_press_event",
390                      G_CALLBACK(key_event), ctx);
391     g_signal_connect(G_OBJECT(ctx->promptlabel), "key_release_event",
392                      G_CALLBACK(key_event), ctx);
393 #if GTK_CHECK_VERSION(2,0,0)
394     gtk_im_context_set_client_window(ctx->imc,
395                                      gtk_widget_get_window(ctx->dialog));
396 #endif
397     gtk_widget_show(ctx->promptlabel);
398
399     return NULL;
400 }
401
402 static void gtk_askpass_cleanup(struct askpass_ctx *ctx)
403 {
404 #if GTK_CHECK_VERSION(3,0,0)
405     gdk_device_ungrab(ctx->keyboard, GDK_CURRENT_TIME);
406 #else
407     gdk_keyboard_ungrab(GDK_CURRENT_TIME);
408 #endif
409     gtk_grab_remove(ctx->promptlabel);
410
411     if (ctx->passphrase) {
412         assert(ctx->passlen < ctx->passsize);
413         ctx->passphrase[ctx->passlen] = '\0';
414     }
415
416     gtk_widget_destroy(ctx->dialog);
417 }
418
419 static int setup_gtk(const char *display)
420 {
421     static int gtk_initialised = FALSE;
422     int argc;
423     char *real_argv[3];
424     char **argv = real_argv;
425     int ret;
426
427     if (gtk_initialised)
428         return TRUE;
429
430     argc = 0;
431     argv[argc++] = dupstr("dummy");
432     argv[argc++] = dupprintf("--display=%s", display);
433     argv[argc] = NULL;
434     ret = gtk_init_check(&argc, &argv);
435     while (argc > 0)
436         sfree(argv[--argc]);
437
438     gtk_initialised = ret;
439     return ret;
440 }
441
442 char *gtk_askpass_main(const char *display, const char *wintitle,
443                        const char *prompt, int *success)
444 {
445     struct askpass_ctx actx, *ctx = &actx;
446     const char *err;
447
448     /* In case gtk_init hasn't been called yet by the program */
449     if (!setup_gtk(display)) {
450         *success = FALSE;
451         return dupstr("unable to initialise GTK");
452     }
453
454     if ((err = gtk_askpass_setup(ctx, wintitle, prompt)) != NULL) {
455         *success = FALSE;
456         return dupprintf("%s", err);
457     }
458     gtk_main();
459     gtk_askpass_cleanup(ctx);
460
461     if (ctx->passphrase) {
462         *success = TRUE;
463         return ctx->passphrase;
464     } else {
465         *success = FALSE;
466         return dupstr("passphrase input cancelled");
467     }
468 }
469
470 #ifdef TEST_ASKPASS
471 void modalfatalbox(const char *p, ...)
472 {
473     va_list ap;
474     fprintf(stderr, "FATAL ERROR: ");
475     va_start(ap, p);
476     vfprintf(stderr, p, ap);
477     va_end(ap);
478     fputc('\n', stderr);
479     exit(1);
480 }
481
482 int main(int argc, char **argv)
483 {
484     int success, exitcode;
485     char *ret;
486
487     gtk_init(&argc, &argv);
488
489     if (argc != 2) {
490         success = FALSE;
491         ret = dupprintf("usage: %s <prompt text>", argv[0]);
492     } else {
493         srand(time(NULL));
494         ret = gtk_askpass_main(argv[1], &success);
495     }
496
497     if (!success) {
498         fputs(ret, stderr);
499         fputc('\n', stderr);
500         exitcode = 1;
501     } else {
502         fputs(ret, stdout);
503         fputc('\n', stdout);
504         exitcode = 0;
505     }
506
507     smemclr(ret, strlen(ret));
508     return exitcode;
509 }
510 #endif