Patches for GTK 2. Puzzles already _built_ under GTK 2, but now it
[sgt/puzzles] / gtk.c
1 /*
2 * gtk.c: GTK front end for my puzzle collection.
3 */
4
5 #include <stdio.h>
6 #include <assert.h>
7 #include <stdlib.h>
8 #include <time.h>
9 #include <stdarg.h>
10 #include <string.h>
11
12 #include <sys/time.h>
13
14 #include <gtk/gtk.h>
15 #include <gdk/gdkkeysyms.h>
16
17 #if GTK_CHECK_VERSION(2,0,0) && !defined HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
18 #include <gdk/gdkx.h>
19 #include <X11/Xlib.h>
20 #endif
21
22 #include "puzzles.h"
23
24 /* ----------------------------------------------------------------------
25 * Error reporting functions used elsewhere.
26 */
27
28 void fatal(char *fmt, ...)
29 {
30 va_list ap;
31
32 fprintf(stderr, "fatal error: ");
33
34 va_start(ap, fmt);
35 vfprintf(stderr, fmt, ap);
36 va_end(ap);
37
38 fprintf(stderr, "\n");
39 exit(1);
40 }
41
42 /* ----------------------------------------------------------------------
43 * GTK front end to puzzles.
44 */
45
46 struct font {
47 GdkFont *font;
48 int type;
49 int size;
50 };
51
52 /*
53 * This structure holds all the data relevant to a single window.
54 * In principle this would allow us to open multiple independent
55 * puzzle windows, although I can't currently see any real point in
56 * doing so. I'm just coding cleanly because there's no
57 * particularly good reason not to.
58 */
59 struct frontend {
60 GtkWidget *window;
61 GtkWidget *area;
62 GtkWidget *statusbar;
63 guint statusctx;
64 GdkPixmap *pixmap;
65 GdkColor *colours;
66 int ncolours;
67 GdkColormap *colmap;
68 int w, h;
69 midend_data *me;
70 GdkGC *gc;
71 int bbox_l, bbox_r, bbox_u, bbox_d;
72 int timer_active, timer_id;
73 struct timeval last_time;
74 struct font *fonts;
75 int nfonts, fontsize;
76 config_item *cfg;
77 int cfg_which, cfgret;
78 GtkWidget *cfgbox;
79 };
80
81 void get_random_seed(void **randseed, int *randseedsize)
82 {
83 time_t *tp = snew(time_t);
84 time(tp);
85 *randseed = (void *)tp;
86 *randseedsize = sizeof(time_t);
87 }
88
89 void frontend_default_colour(frontend *fe, float *output)
90 {
91 GdkColor col = fe->window->style->bg[GTK_STATE_NORMAL];
92 output[0] = col.red / 65535.0;
93 output[1] = col.green / 65535.0;
94 output[2] = col.blue / 65535.0;
95 }
96
97 void status_bar(frontend *fe, char *text)
98 {
99 assert(fe->statusbar);
100
101 gtk_statusbar_pop(GTK_STATUSBAR(fe->statusbar), fe->statusctx);
102 gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, text);
103 }
104
105 void start_draw(frontend *fe)
106 {
107 fe->gc = gdk_gc_new(fe->area->window);
108 fe->bbox_l = fe->w;
109 fe->bbox_r = 0;
110 fe->bbox_u = fe->h;
111 fe->bbox_d = 0;
112 }
113
114 void clip(frontend *fe, int x, int y, int w, int h)
115 {
116 GdkRectangle rect;
117
118 rect.x = x;
119 rect.y = y;
120 rect.width = w;
121 rect.height = h;
122
123 gdk_gc_set_clip_rectangle(fe->gc, &rect);
124 }
125
126 void unclip(frontend *fe)
127 {
128 GdkRectangle rect;
129
130 rect.x = 0;
131 rect.y = 0;
132 rect.width = fe->w;
133 rect.height = fe->h;
134
135 gdk_gc_set_clip_rectangle(fe->gc, &rect);
136 }
137
138 void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
139 int align, int colour, char *text)
140 {
141 int i;
142
143 /*
144 * Find or create the font.
145 */
146 for (i = 0; i < fe->nfonts; i++)
147 if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize)
148 break;
149
150 if (i == fe->nfonts) {
151 if (fe->fontsize <= fe->nfonts) {
152 fe->fontsize = fe->nfonts + 10;
153 fe->fonts = sresize(fe->fonts, fe->fontsize, struct font);
154 }
155
156 fe->nfonts++;
157
158 fe->fonts[i].type = fonttype;
159 fe->fonts[i].size = fontsize;
160
161 #if GTK_CHECK_VERSION(2,0,0)
162 /*
163 * Use Pango to find the closest match to the requested
164 * font.
165 */
166 {
167 PangoFontDescription *fd;
168
169 fd = pango_font_description_new();
170 /* `Monospace' and `Sans' are meta-families guaranteed to exist */
171 pango_font_description_set_family(fd, fonttype == FONT_FIXED ?
172 "Monospace" : "Sans");
173 /*
174 * I found some online Pango documentation which
175 * described a function called
176 * pango_font_description_set_absolute_size(), which is
177 * _exactly_ what I want here. Unfortunately, none of
178 * my local Pango installations have it (presumably
179 * they're too old), so I'm going to have to hack round
180 * it by figuring out the point size myself. This
181 * limits me to X and probably also breaks in later
182 * Pango installations, so ideally I should add another
183 * CHECK_VERSION type ifdef and use set_absolute_size
184 * where available. All very annoying.
185 */
186 #ifdef HAVE_SENSIBLE_ABSOLUTE_SIZE_FUNCTION
187 pango_font_description_set_absolute_size(fd, PANGO_SCALE*fontsize);
188 #else
189 {
190 Display *d = GDK_DISPLAY();
191 int s = DefaultScreen(d);
192 double resolution =
193 (PANGO_SCALE * 72.27 / 25.4) *
194 ((double) DisplayWidthMM(d, s) / DisplayWidth (d, s));
195 pango_font_description_set_size(fd, resolution * fontsize);
196 }
197 #endif
198 fe->fonts[i].font = gdk_font_from_description(fd);
199 pango_font_description_free(fd);
200 }
201
202 if (!fe->fonts[i].font)
203 #endif
204 /*
205 * In GTK 1.2, I don't know of any plausible way to
206 * pick a suitable font, so I'm just going to be
207 * tedious.
208 *
209 * This is also fallback code called if the Pango
210 * approach fails to find an appropriate font.
211 */
212 fe->fonts[i].font = gdk_font_load(fonttype == FONT_FIXED ?
213 "fixed" : "variable");
214 }
215
216 /*
217 * Find string dimensions and process alignment.
218 */
219 {
220 int lb, rb, wid, asc, desc;
221
222 gdk_string_extents(fe->fonts[i].font, text,
223 &lb, &rb, &wid, &asc, &desc);
224 if (align & ALIGN_VCENTRE)
225 y += asc - (asc+desc)/2;
226
227 if (align & ALIGN_HCENTRE)
228 x -= wid / 2;
229 else if (align & ALIGN_HRIGHT)
230 x -= wid;
231
232 }
233
234 /*
235 * Set colour and actually draw text.
236 */
237 gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
238 gdk_draw_string(fe->pixmap, fe->fonts[i].font, fe->gc, x, y, text);
239 }
240
241 void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
242 {
243 gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
244 gdk_draw_rectangle(fe->pixmap, fe->gc, 1, x, y, w, h);
245 }
246
247 void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
248 {
249 gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
250 gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
251 }
252
253 void draw_polygon(frontend *fe, int *coords, int npoints,
254 int fill, int colour)
255 {
256 GdkPoint *points = snewn(npoints, GdkPoint);
257 int i;
258
259 for (i = 0; i < npoints; i++) {
260 points[i].x = coords[i*2];
261 points[i].y = coords[i*2+1];
262 }
263
264 gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
265 gdk_draw_polygon(fe->pixmap, fe->gc, fill, points, npoints);
266
267 sfree(points);
268 }
269
270 void draw_update(frontend *fe, int x, int y, int w, int h)
271 {
272 if (fe->bbox_l > x ) fe->bbox_l = x ;
273 if (fe->bbox_r < x+w) fe->bbox_r = x+w;
274 if (fe->bbox_u > y ) fe->bbox_u = y ;
275 if (fe->bbox_d < y+h) fe->bbox_d = y+h;
276 }
277
278 void end_draw(frontend *fe)
279 {
280 gdk_gc_unref(fe->gc);
281 fe->gc = NULL;
282
283 if (fe->bbox_l < fe->bbox_r && fe->bbox_u < fe->bbox_d) {
284 gdk_draw_pixmap(fe->area->window,
285 fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
286 fe->pixmap,
287 fe->bbox_l, fe->bbox_u,
288 fe->bbox_l, fe->bbox_u,
289 fe->bbox_r - fe->bbox_l, fe->bbox_d - fe->bbox_u);
290 }
291 }
292
293 static void destroy(GtkWidget *widget, gpointer data)
294 {
295 frontend *fe = (frontend *)data;
296 deactivate_timer(fe);
297 gtk_main_quit();
298 }
299
300 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
301 {
302 frontend *fe = (frontend *)data;
303 int keyval;
304
305 if (!fe->pixmap)
306 return TRUE;
307
308 if (event->string[0] && !event->string[1])
309 keyval = (unsigned char)event->string[0];
310 else if (event->keyval == GDK_Up || event->keyval == GDK_KP_Up ||
311 event->keyval == GDK_KP_8)
312 keyval = CURSOR_UP;
313 else if (event->keyval == GDK_Down || event->keyval == GDK_KP_Down ||
314 event->keyval == GDK_KP_2)
315 keyval = CURSOR_DOWN;
316 else if (event->keyval == GDK_Left || event->keyval == GDK_KP_Left ||
317 event->keyval == GDK_KP_4)
318 keyval = CURSOR_LEFT;
319 else if (event->keyval == GDK_Right || event->keyval == GDK_KP_Right ||
320 event->keyval == GDK_KP_6)
321 keyval = CURSOR_RIGHT;
322 else if (event->keyval == GDK_KP_Home || event->keyval == GDK_KP_7)
323 keyval = CURSOR_UP_LEFT;
324 else if (event->keyval == GDK_KP_End || event->keyval == GDK_KP_1)
325 keyval = CURSOR_DOWN_LEFT;
326 else if (event->keyval == GDK_KP_Page_Up || event->keyval == GDK_KP_9)
327 keyval = CURSOR_UP_RIGHT;
328 else if (event->keyval == GDK_KP_Page_Down || event->keyval == GDK_KP_3)
329 keyval = CURSOR_DOWN_RIGHT;
330 else
331 keyval = -1;
332
333 if (keyval >= 0 &&
334 !midend_process_key(fe->me, 0, 0, keyval))
335 gtk_widget_destroy(fe->window);
336
337 return TRUE;
338 }
339
340 static gint button_event(GtkWidget *widget, GdkEventButton *event,
341 gpointer data)
342 {
343 frontend *fe = (frontend *)data;
344 int button;
345
346 if (!fe->pixmap)
347 return TRUE;
348
349 if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE)
350 return TRUE;
351
352 if (event->button == 2 || (event->state & GDK_SHIFT_MASK))
353 button = MIDDLE_BUTTON;
354 else if (event->button == 1)
355 button = LEFT_BUTTON;
356 else if (event->button == 3)
357 button = RIGHT_BUTTON;
358 else
359 return FALSE; /* don't even know what button! */
360
361 if (event->type == GDK_BUTTON_RELEASE)
362 button += LEFT_RELEASE - LEFT_BUTTON;
363
364 if (!midend_process_key(fe->me, event->x, event->y, button))
365 gtk_widget_destroy(fe->window);
366
367 return TRUE;
368 }
369
370 static gint motion_event(GtkWidget *widget, GdkEventMotion *event,
371 gpointer data)
372 {
373 frontend *fe = (frontend *)data;
374 int button;
375
376 if (!fe->pixmap)
377 return TRUE;
378
379 if (event->state & (GDK_BUTTON2_MASK | GDK_SHIFT_MASK))
380 button = MIDDLE_DRAG;
381 else if (event->state & GDK_BUTTON1_MASK)
382 button = LEFT_DRAG;
383 else if (event->state & GDK_BUTTON3_MASK)
384 button = RIGHT_DRAG;
385 else
386 return FALSE; /* don't even know what button! */
387
388 if (!midend_process_key(fe->me, event->x, event->y, button))
389 gtk_widget_destroy(fe->window);
390
391 return TRUE;
392 }
393
394 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
395 gpointer data)
396 {
397 frontend *fe = (frontend *)data;
398
399 if (fe->pixmap) {
400 gdk_draw_pixmap(widget->window,
401 widget->style->fg_gc[GTK_WIDGET_STATE(widget)],
402 fe->pixmap,
403 event->area.x, event->area.y,
404 event->area.x, event->area.y,
405 event->area.width, event->area.height);
406 }
407 return TRUE;
408 }
409
410 static gint map_window(GtkWidget *widget, GdkEvent *event,
411 gpointer data)
412 {
413 frontend *fe = (frontend *)data;
414
415 /*
416 * Apparently we need to do this because otherwise the status
417 * bar will fail to update immediately. Annoying, but there we
418 * go.
419 */
420 gtk_widget_queue_draw(fe->window);
421
422 return TRUE;
423 }
424
425 static gint configure_area(GtkWidget *widget,
426 GdkEventConfigure *event, gpointer data)
427 {
428 frontend *fe = (frontend *)data;
429 GdkGC *gc;
430
431 if (fe->pixmap)
432 gdk_pixmap_unref(fe->pixmap);
433
434 fe->pixmap = gdk_pixmap_new(widget->window, fe->w, fe->h, -1);
435
436 gc = gdk_gc_new(fe->area->window);
437 gdk_gc_set_foreground(gc, &fe->colours[0]);
438 gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->w, fe->h);
439 gdk_gc_unref(gc);
440
441 midend_redraw(fe->me);
442
443 return TRUE;
444 }
445
446 static gint timer_func(gpointer data)
447 {
448 frontend *fe = (frontend *)data;
449
450 if (fe->timer_active) {
451 struct timeval now;
452 float elapsed;
453 gettimeofday(&now, NULL);
454 elapsed = ((now.tv_usec - fe->last_time.tv_usec) * 0.000001F +
455 (now.tv_sec - fe->last_time.tv_sec));
456 midend_timer(fe->me, elapsed); /* may clear timer_active */
457 fe->last_time = now;
458 }
459
460 return fe->timer_active;
461 }
462
463 void deactivate_timer(frontend *fe)
464 {
465 if (fe->timer_active)
466 gtk_timeout_remove(fe->timer_id);
467 fe->timer_active = FALSE;
468 }
469
470 void activate_timer(frontend *fe)
471 {
472 if (!fe->timer_active) {
473 fe->timer_id = gtk_timeout_add(20, timer_func, fe);
474 gettimeofday(&fe->last_time, NULL);
475 }
476 fe->timer_active = TRUE;
477 }
478
479 static void window_destroy(GtkWidget *widget, gpointer data)
480 {
481 gtk_main_quit();
482 }
483
484 static void errmsg_button_clicked(GtkButton *button, gpointer data)
485 {
486 gtk_widget_destroy(GTK_WIDGET(data));
487 }
488
489 static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
490 {
491 GtkObject *cancelbutton = GTK_OBJECT(data);
492
493 /*
494 * `Escape' effectively clicks the cancel button
495 */
496 if (event->keyval == GDK_Escape) {
497 gtk_signal_emit_by_name(GTK_OBJECT(cancelbutton), "clicked");
498 return TRUE;
499 }
500
501 return FALSE;
502 }
503
504 void error_box(GtkWidget *parent, char *msg)
505 {
506 GtkWidget *window, *hbox, *text, *ok;
507
508 window = gtk_dialog_new();
509 text = gtk_label_new(msg);
510 gtk_misc_set_alignment(GTK_MISC(text), 0.0, 0.0);
511 hbox = gtk_hbox_new(FALSE, 0);
512 gtk_box_pack_start(GTK_BOX(hbox), text, FALSE, FALSE, 20);
513 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(window)->vbox),
514 hbox, FALSE, FALSE, 20);
515 gtk_widget_show(text);
516 gtk_widget_show(hbox);
517 gtk_window_set_title(GTK_WINDOW(window), "Error");
518 gtk_label_set_line_wrap(GTK_LABEL(text), TRUE);
519 ok = gtk_button_new_with_label("OK");
520 gtk_box_pack_end(GTK_BOX(GTK_DIALOG(window)->action_area),
521 ok, FALSE, FALSE, 0);
522 gtk_widget_show(ok);
523 GTK_WIDGET_SET_FLAGS(ok, GTK_CAN_DEFAULT);
524 gtk_window_set_default(GTK_WINDOW(window), ok);
525 gtk_signal_connect(GTK_OBJECT(ok), "clicked",
526 GTK_SIGNAL_FUNC(errmsg_button_clicked), window);
527 gtk_signal_connect(GTK_OBJECT(window), "destroy",
528 GTK_SIGNAL_FUNC(window_destroy), NULL);
529 gtk_signal_connect(GTK_OBJECT(window), "key_press_event",
530 GTK_SIGNAL_FUNC(win_key_press), ok);
531 gtk_window_set_modal(GTK_WINDOW(window), TRUE);
532 gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(parent));
533 //set_transient_window_pos(parent, window);
534 gtk_widget_show(window);
535 gtk_main();
536 }
537
538 static void config_ok_button_clicked(GtkButton *button, gpointer data)
539 {
540 frontend *fe = (frontend *)data;
541 char *err;
542
543 err = midend_set_config(fe->me, fe->cfg_which, fe->cfg);
544
545 if (err)
546 error_box(fe->cfgbox, err);
547 else {
548 fe->cfgret = TRUE;
549 gtk_widget_destroy(fe->cfgbox);
550 }
551 }
552
553 static void config_cancel_button_clicked(GtkButton *button, gpointer data)
554 {
555 frontend *fe = (frontend *)data;
556
557 gtk_widget_destroy(fe->cfgbox);
558 }
559
560 static int editbox_key(GtkWidget *widget, GdkEventKey *event, gpointer data)
561 {
562 /*
563 * GtkEntry has a nasty habit of eating the Return key, which
564 * is unhelpful since it doesn't actually _do_ anything with it
565 * (it calls gtk_widget_activate, but our edit boxes never need
566 * activating). So I catch Return before GtkEntry sees it, and
567 * pass it straight on to the parent widget. Effect: hitting
568 * Return in an edit box will now activate the default button
569 * in the dialog just like it will everywhere else.
570 */
571 if (event->keyval == GDK_Return && widget->parent != NULL) {
572 gint return_val;
573 gtk_signal_emit_stop_by_name(GTK_OBJECT(widget), "key_press_event");
574 gtk_signal_emit_by_name(GTK_OBJECT(widget->parent), "key_press_event",
575 event, &return_val);
576 return return_val;
577 }
578 return FALSE;
579 }
580
581 static void editbox_changed(GtkEditable *ed, gpointer data)
582 {
583 config_item *i = (config_item *)data;
584
585 sfree(i->sval);
586 i->sval = dupstr(gtk_entry_get_text(GTK_ENTRY(ed)));
587 }
588
589 static void button_toggled(GtkToggleButton *tb, gpointer data)
590 {
591 config_item *i = (config_item *)data;
592
593 i->ival = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(tb));
594 }
595
596 static void droplist_sel(GtkMenuItem *item, gpointer data)
597 {
598 config_item *i = (config_item *)data;
599
600 i->ival = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(item),
601 "user-data"));
602 }
603
604 static int get_config(frontend *fe, int which)
605 {
606 GtkWidget *w, *table, *cancel;
607 char *title;
608 config_item *i;
609 int y;
610
611 fe->cfg = midend_get_config(fe->me, which, &title);
612 fe->cfg_which = which;
613 fe->cfgret = FALSE;
614
615 fe->cfgbox = gtk_dialog_new();
616 gtk_window_set_title(GTK_WINDOW(fe->cfgbox), title);
617 sfree(title);
618
619 w = gtk_button_new_with_label("OK");
620 gtk_box_pack_end(GTK_BOX(GTK_DIALOG(fe->cfgbox)->action_area),
621 w, FALSE, FALSE, 0);
622 gtk_widget_show(w);
623 GTK_WIDGET_SET_FLAGS(w, GTK_CAN_DEFAULT);
624 gtk_window_set_default(GTK_WINDOW(fe->cfgbox), w);
625 gtk_signal_connect(GTK_OBJECT(w), "clicked",
626 GTK_SIGNAL_FUNC(config_ok_button_clicked), fe);
627
628 w = gtk_button_new_with_label("Cancel");
629 gtk_box_pack_end(GTK_BOX(GTK_DIALOG(fe->cfgbox)->action_area),
630 w, FALSE, FALSE, 0);
631 gtk_widget_show(w);
632 gtk_signal_connect(GTK_OBJECT(w), "clicked",
633 GTK_SIGNAL_FUNC(config_cancel_button_clicked), fe);
634 cancel = w;
635
636 table = gtk_table_new(1, 2, FALSE);
637 y = 0;
638 gtk_box_pack_end(GTK_BOX(GTK_DIALOG(fe->cfgbox)->vbox),
639 table, FALSE, FALSE, 0);
640 gtk_widget_show(table);
641
642 for (i = fe->cfg; i->type != C_END; i++) {
643 gtk_table_resize(GTK_TABLE(table), y+1, 2);
644
645 switch (i->type) {
646 case C_STRING:
647 /*
648 * Edit box with a label beside it.
649 */
650
651 w = gtk_label_new(i->name);
652 gtk_misc_set_alignment(GTK_MISC(w), 0.0, 0.5);
653 gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
654 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
655 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
656 3, 3);
657 gtk_widget_show(w);
658
659 w = gtk_entry_new();
660 gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
661 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
662 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
663 3, 3);
664 gtk_entry_set_text(GTK_ENTRY(w), i->sval);
665 gtk_signal_connect(GTK_OBJECT(w), "changed",
666 GTK_SIGNAL_FUNC(editbox_changed), i);
667 gtk_signal_connect(GTK_OBJECT(w), "key_press_event",
668 GTK_SIGNAL_FUNC(editbox_key), NULL);
669 gtk_widget_show(w);
670
671 break;
672
673 case C_BOOLEAN:
674 /*
675 * Simple checkbox.
676 */
677 w = gtk_check_button_new_with_label(i->name);
678 gtk_signal_connect(GTK_OBJECT(w), "toggled",
679 GTK_SIGNAL_FUNC(button_toggled), i);
680 gtk_table_attach(GTK_TABLE(table), w, 0, 2, y, y+1,
681 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
682 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
683 3, 3);
684 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), i->ival);
685 gtk_widget_show(w);
686 break;
687
688 case C_CHOICES:
689 /*
690 * Drop-down list (GtkOptionMenu).
691 */
692
693 w = gtk_label_new(i->name);
694 gtk_misc_set_alignment(GTK_MISC(w), 0.0, 0.5);
695 gtk_table_attach(GTK_TABLE(table), w, 0, 1, y, y+1,
696 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
697 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
698 3, 3);
699 gtk_widget_show(w);
700
701 w = gtk_option_menu_new();
702 gtk_table_attach(GTK_TABLE(table), w, 1, 2, y, y+1,
703 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
704 GTK_EXPAND | GTK_SHRINK | GTK_FILL,
705 3, 3);
706 gtk_widget_show(w);
707
708 {
709 int c, val;
710 char *p, *q, *name;
711 GtkWidget *menuitem;
712 GtkWidget *menu = gtk_menu_new();
713
714 gtk_option_menu_set_menu(GTK_OPTION_MENU(w), menu);
715
716 c = *i->sval;
717 p = i->sval+1;
718 val = 0;
719
720 while (*p) {
721 q = p;
722 while (*q && *q != c)
723 q++;
724
725 name = snewn(q-p+1, char);
726 strncpy(name, p, q-p);
727 name[q-p] = '\0';
728
729 if (*q) q++; /* eat delimiter */
730
731 menuitem = gtk_menu_item_new_with_label(name);
732 gtk_container_add(GTK_CONTAINER(menu), menuitem);
733 gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
734 GINT_TO_POINTER(val));
735 gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
736 GTK_SIGNAL_FUNC(droplist_sel), i);
737 gtk_widget_show(menuitem);
738
739 val++;
740
741 p = q;
742 }
743
744 gtk_option_menu_set_history(GTK_OPTION_MENU(w), i->ival);
745 }
746
747 break;
748 }
749
750 y++;
751 }
752
753 gtk_signal_connect(GTK_OBJECT(fe->cfgbox), "destroy",
754 GTK_SIGNAL_FUNC(window_destroy), NULL);
755 gtk_signal_connect(GTK_OBJECT(fe->cfgbox), "key_press_event",
756 GTK_SIGNAL_FUNC(win_key_press), cancel);
757 gtk_window_set_modal(GTK_WINDOW(fe->cfgbox), TRUE);
758 gtk_window_set_transient_for(GTK_WINDOW(fe->cfgbox),
759 GTK_WINDOW(fe->window));
760 //set_transient_window_pos(fe->window, fe->cfgbox);
761 gtk_widget_show(fe->cfgbox);
762 gtk_main();
763
764 free_cfg(fe->cfg);
765
766 return fe->cfgret;
767 }
768
769 static void menu_key_event(GtkMenuItem *menuitem, gpointer data)
770 {
771 frontend *fe = (frontend *)data;
772 int key = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(menuitem),
773 "user-data"));
774 if (!midend_process_key(fe->me, 0, 0, key))
775 gtk_widget_destroy(fe->window);
776 }
777
778 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
779 {
780 frontend *fe = (frontend *)data;
781 game_params *params =
782 (game_params *)gtk_object_get_data(GTK_OBJECT(menuitem), "user-data");
783 int x, y;
784
785 midend_set_params(fe->me, params);
786 midend_new_game(fe->me);
787 midend_size(fe->me, &x, &y);
788 gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
789 fe->w = x;
790 fe->h = y;
791 }
792
793 static void menu_config_event(GtkMenuItem *menuitem, gpointer data)
794 {
795 frontend *fe = (frontend *)data;
796 int which = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(menuitem),
797 "user-data"));
798 int x, y;
799
800 if (!get_config(fe, which))
801 return;
802
803 midend_new_game(fe->me);
804 midend_size(fe->me, &x, &y);
805 gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
806 fe->w = x;
807 fe->h = y;
808 }
809
810 static GtkWidget *add_menu_item_with_key(frontend *fe, GtkContainer *cont,
811 char *text, int key)
812 {
813 GtkWidget *menuitem = gtk_menu_item_new_with_label(text);
814 gtk_container_add(cont, menuitem);
815 gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
816 GINT_TO_POINTER(key));
817 gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
818 GTK_SIGNAL_FUNC(menu_key_event), fe);
819 gtk_widget_show(menuitem);
820 return menuitem;
821 }
822
823 static void add_menu_separator(GtkContainer *cont)
824 {
825 GtkWidget *menuitem = gtk_menu_item_new();
826 gtk_container_add(cont, menuitem);
827 gtk_widget_show(menuitem);
828 }
829
830 static frontend *new_window(char *game_id, char **error)
831 {
832 frontend *fe;
833 GtkBox *vbox;
834 GtkWidget *menubar, *menu, *menuitem;
835 int x, y, n;
836
837 fe = snew(frontend);
838
839 fe->me = midend_new(fe, &thegame);
840 if (game_id) {
841 *error = midend_game_id(fe->me, game_id, FALSE);
842 if (*error) {
843 midend_free(fe->me);
844 sfree(fe);
845 return NULL;
846 }
847 }
848 midend_new_game(fe->me);
849
850 fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
851 gtk_window_set_title(GTK_WINDOW(fe->window), thegame.name);
852 #if 0
853 gtk_window_set_resizable(GTK_WINDOW(fe->window), FALSE);
854 #else
855 gtk_window_set_policy(GTK_WINDOW(fe->window), FALSE, FALSE, TRUE);
856 #endif
857 vbox = GTK_BOX(gtk_vbox_new(FALSE, 0));
858 gtk_container_add(GTK_CONTAINER(fe->window), GTK_WIDGET(vbox));
859 gtk_widget_show(GTK_WIDGET(vbox));
860
861 menubar = gtk_menu_bar_new();
862 gtk_box_pack_start(vbox, menubar, FALSE, FALSE, 0);
863 gtk_widget_show(menubar);
864
865 menuitem = gtk_menu_item_new_with_label("Game");
866 gtk_container_add(GTK_CONTAINER(menubar), menuitem);
867 gtk_widget_show(menuitem);
868
869 menu = gtk_menu_new();
870 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
871
872 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "New", 'n');
873 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Restart", 'r');
874
875 menuitem = gtk_menu_item_new_with_label("Specific...");
876 gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
877 GINT_TO_POINTER(CFG_SEED));
878 gtk_container_add(GTK_CONTAINER(menu), menuitem);
879 gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
880 GTK_SIGNAL_FUNC(menu_config_event), fe);
881 gtk_widget_show(menuitem);
882
883 if ((n = midend_num_presets(fe->me)) > 0 || thegame.can_configure) {
884 GtkWidget *submenu;
885 int i;
886
887 menuitem = gtk_menu_item_new_with_label("Type");
888 gtk_container_add(GTK_CONTAINER(menubar), menuitem);
889 gtk_widget_show(menuitem);
890
891 submenu = gtk_menu_new();
892 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
893
894 for (i = 0; i < n; i++) {
895 char *name;
896 game_params *params;
897
898 midend_fetch_preset(fe->me, i, &name, &params);
899
900 menuitem = gtk_menu_item_new_with_label(name);
901 gtk_container_add(GTK_CONTAINER(submenu), menuitem);
902 gtk_object_set_data(GTK_OBJECT(menuitem), "user-data", params);
903 gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
904 GTK_SIGNAL_FUNC(menu_preset_event), fe);
905 gtk_widget_show(menuitem);
906 }
907
908 if (thegame.can_configure) {
909 menuitem = gtk_menu_item_new_with_label("Custom...");
910 gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
911 GPOINTER_TO_INT(CFG_SETTINGS));
912 gtk_container_add(GTK_CONTAINER(submenu), menuitem);
913 gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
914 GTK_SIGNAL_FUNC(menu_config_event), fe);
915 gtk_widget_show(menuitem);
916 }
917 }
918
919 add_menu_separator(GTK_CONTAINER(menu));
920 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Undo", 'u');
921 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Redo", '\x12');
922 add_menu_separator(GTK_CONTAINER(menu));
923 add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Exit", 'q');
924
925 {
926 int i, ncolours;
927 float *colours;
928 gboolean *success;
929
930 fe->colmap = gdk_colormap_get_system();
931 colours = midend_colours(fe->me, &ncolours);
932 fe->ncolours = ncolours;
933 fe->colours = snewn(ncolours, GdkColor);
934 for (i = 0; i < ncolours; i++) {
935 fe->colours[i].red = colours[i*3] * 0xFFFF;
936 fe->colours[i].green = colours[i*3+1] * 0xFFFF;
937 fe->colours[i].blue = colours[i*3+2] * 0xFFFF;
938 }
939 success = snewn(ncolours, gboolean);
940 gdk_colormap_alloc_colors(fe->colmap, fe->colours, ncolours,
941 FALSE, FALSE, success);
942 for (i = 0; i < ncolours; i++) {
943 if (!success[i])
944 g_error("couldn't allocate colour %d (#%02x%02x%02x)\n",
945 i, fe->colours[i].red >> 8,
946 fe->colours[i].green >> 8,
947 fe->colours[i].blue >> 8);
948 }
949 }
950
951 if (midend_wants_statusbar(fe->me)) {
952 GtkWidget *viewport;
953 GtkRequisition req;
954
955 viewport = gtk_viewport_new(NULL, NULL);
956 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
957 fe->statusbar = gtk_statusbar_new();
958 gtk_container_add(GTK_CONTAINER(viewport), fe->statusbar);
959 gtk_widget_show(viewport);
960 gtk_box_pack_end(vbox, viewport, FALSE, FALSE, 0);
961 gtk_widget_show(fe->statusbar);
962 fe->statusctx = gtk_statusbar_get_context_id
963 (GTK_STATUSBAR(fe->statusbar), "game");
964 gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx,
965 "test");
966 gtk_widget_size_request(fe->statusbar, &req);
967 #if 0
968 /* For GTK 2.0, should we be using gtk_widget_set_size_request? */
969 #endif
970 gtk_widget_set_usize(viewport, -1, req.height);
971 } else
972 fe->statusbar = NULL;
973
974 fe->area = gtk_drawing_area_new();
975 midend_size(fe->me, &x, &y);
976 gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
977 fe->w = x;
978 fe->h = y;
979
980 gtk_box_pack_end(vbox, fe->area, FALSE, FALSE, 0);
981
982 fe->pixmap = NULL;
983 fe->fonts = NULL;
984 fe->nfonts = fe->fontsize = 0;
985
986 fe->timer_active = FALSE;
987
988 gtk_signal_connect(GTK_OBJECT(fe->window), "destroy",
989 GTK_SIGNAL_FUNC(destroy), fe);
990 gtk_signal_connect(GTK_OBJECT(fe->window), "key_press_event",
991 GTK_SIGNAL_FUNC(key_event), fe);
992 gtk_signal_connect(GTK_OBJECT(fe->area), "button_press_event",
993 GTK_SIGNAL_FUNC(button_event), fe);
994 gtk_signal_connect(GTK_OBJECT(fe->area), "button_release_event",
995 GTK_SIGNAL_FUNC(button_event), fe);
996 gtk_signal_connect(GTK_OBJECT(fe->area), "motion_notify_event",
997 GTK_SIGNAL_FUNC(motion_event), fe);
998 gtk_signal_connect(GTK_OBJECT(fe->area), "expose_event",
999 GTK_SIGNAL_FUNC(expose_area), fe);
1000 gtk_signal_connect(GTK_OBJECT(fe->window), "map_event",
1001 GTK_SIGNAL_FUNC(map_window), fe);
1002 gtk_signal_connect(GTK_OBJECT(fe->area), "configure_event",
1003 GTK_SIGNAL_FUNC(configure_area), fe);
1004
1005 gtk_widget_add_events(GTK_WIDGET(fe->area),
1006 GDK_BUTTON_PRESS_MASK |
1007 GDK_BUTTON_RELEASE_MASK |
1008 GDK_BUTTON_MOTION_MASK);
1009
1010 gtk_widget_show(fe->area);
1011 gtk_widget_show(fe->window);
1012
1013 return fe;
1014 }
1015
1016 int main(int argc, char **argv)
1017 {
1018 char *pname = argv[0];
1019 char *error;
1020
1021 gtk_init(&argc, &argv);
1022
1023 if (!new_window(argc > 1 ? argv[1] : NULL, &error)) {
1024 fprintf(stderr, "%s: %s\n", pname, error);
1025 return 1;
1026 }
1027
1028 gtk_main();
1029
1030 return 0;
1031 }