X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=blobdiff_plain;f=gtk.c;h=dd2ebe6389665ee9f5669c875b2f65262f1fe27a;hb=refs%2Fheads%2Ffor-aldabra;hp=26b3ce0dfe44c6c654b8d7f6b3f80d36362bf49c;hpb=41cc7c868f72e3131a3d45e38994e9d473824bd1;p=sgt-puzzles.git diff --git a/gtk.c b/gtk.c index 26b3ce0..dd2ebe6 100644 --- a/gtk.c +++ b/gtk.c @@ -2,6 +2,10 @@ * gtk.c: GTK front end for my puzzle collection. */ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 1 /* for PATH_MAX */ +#endif + #include #include #include @@ -10,6 +14,9 @@ #include #include #include +#include +#include +#include #include #include @@ -140,10 +147,13 @@ struct font { */ struct frontend { GtkWidget *window; - GtkAccelGroup *accelgroup; + GtkAccelGroup *dummy_accelgroup; GtkWidget *area; GtkWidget *statusbar; GtkWidget *menubar; +#if GTK_CHECK_VERSION(3,20,0) + GtkCssProvider *css_provider; +#endif guint statusctx; int w, h; midend *me; @@ -179,7 +189,6 @@ struct frontend { char *filesel_name; #endif GSList *preset_radio; - int n_preset_menu_items; int preset_threaded; GtkWidget *preset_custom; GtkWidget *copy_menu_item; @@ -290,7 +299,27 @@ static void set_colour(frontend *fe, int colour) static void set_window_background(frontend *fe, int colour) { -#if GTK_CHECK_VERSION(3,0,0) +#if GTK_CHECK_VERSION(3,20,0) + char css_buf[512]; + sprintf(css_buf, ".background { " + "background-color: #%02x%02x%02x; }", + (unsigned)(fe->colours[3*colour + 0] * 255), + (unsigned)(fe->colours[3*colour + 1] * 255), + (unsigned)(fe->colours[3*colour + 2] * 255)); + if (!fe->css_provider) + fe->css_provider = gtk_css_provider_new(); + if (!gtk_css_provider_load_from_data( + GTK_CSS_PROVIDER(fe->css_provider), css_buf, -1, NULL)) + assert(0 && "Couldn't load CSS"); + gtk_style_context_add_provider( + gtk_widget_get_style_context(fe->window), + GTK_STYLE_PROVIDER(fe->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + gtk_style_context_add_provider( + gtk_widget_get_style_context(fe->area), + GTK_STYLE_PROVIDER(fe->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +#elif GTK_CHECK_VERSION(3,0,0) GdkRGBA rgba; rgba.red = fe->colours[3*colour + 0]; rgba.green = fe->colours[3*colour + 1]; @@ -439,11 +468,13 @@ static void clear_backing_store(frontend *fe) fe->image = NULL; } -static void wipe_and_destroy_cairo(frontend *fe, cairo_t *cr) +static void wipe_and_maybe_destroy_cairo(frontend *fe, cairo_t *cr, + int destroy) { cairo_set_source_rgb(cr, fe->colours[0], fe->colours[1], fe->colours[2]); cairo_paint(cr); - cairo_destroy(cr); + if (destroy) + cairo_destroy(cr); } static void setup_backing_store(frontend *fe) @@ -455,12 +486,29 @@ static void setup_backing_store(frontend *fe) fe->image = cairo_image_surface_create(CAIRO_FORMAT_RGB24, fe->pw, fe->ph); - wipe_and_destroy_cairo(fe, cairo_create(fe->image)); + wipe_and_maybe_destroy_cairo(fe, cairo_create(fe->image), TRUE); #ifndef USE_CAIRO_WITHOUT_PIXMAP - wipe_and_destroy_cairo(fe, gdk_cairo_create(fe->pixmap)); + wipe_and_maybe_destroy_cairo(fe, gdk_cairo_create(fe->pixmap), TRUE); +#endif +#if GTK_CHECK_VERSION(3,22,0) + { + GdkWindow *gdkwin; + cairo_region_t *region; + GdkDrawingContext *drawctx; + cairo_t *cr; + + gdkwin = gtk_widget_get_window(fe->area); + region = gdk_window_get_clip_region(gdkwin); + drawctx = gdk_window_begin_draw_frame(gdkwin, region); + cr = gdk_drawing_context_get_cairo_context(drawctx); + wipe_and_maybe_destroy_cairo(fe, cr, FALSE); + gdk_window_end_draw_frame(gdkwin, drawctx); + cairo_region_destroy(region); + } +#else + wipe_and_maybe_destroy_cairo( + fe, gdk_cairo_create(gtk_widget_get_window(fe->area)), TRUE); #endif - wipe_and_destroy_cairo(fe, gdk_cairo_create - (gtk_widget_get_window(fe->area))); } static int backing_store_ok(frontend *fe) @@ -1119,16 +1167,6 @@ static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data) if (!backing_store_ok(fe)) return TRUE; -#if !GTK_CHECK_VERSION(2,0,0) - /* Gtk 1.2 passes a key event to this function even if it's also - * defined as an accelerator. - * Gtk 2 doesn't do this, and this function appears not to exist there. */ - if (fe->accelgroup && - gtk_accel_group_get_entry(fe->accelgroup, - event->keyval, event->state)) - return TRUE; -#endif - /* Handle mnemonics. */ if (gtk_window_activate_key(GTK_WINDOW(fe->window), event)) return TRUE; @@ -1175,6 +1213,8 @@ static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data) event->keyval == GDK_KEY_Delete || event->keyval == GDK_KEY_KP_Delete) keyval = '\177'; + else if ((event->keyval == 'z' || event->keyval == 'Z') && shift && ctrl) + keyval = UI_REDO; else if (event->string[0] && !event->string[1]) keyval = (unsigned char)event->string[0]; else @@ -1205,10 +1245,14 @@ static gint button_event(GtkWidget *widget, GdkEventButton *event, button = RIGHT_BUTTON; else if (event->button == 1) button = LEFT_BUTTON; + else if (event->button == 8 && event->type == GDK_BUTTON_PRESS) + button = 'u'; + else if (event->button == 9 && event->type == GDK_BUTTON_PRESS) + button = 'r'; else return FALSE; /* don't even know what button! */ - if (event->type == GDK_BUTTON_RELEASE) + if (event->type == GDK_BUTTON_RELEASE && button >= LEFT_BUTTON) button += LEFT_RELEASE - LEFT_BUTTON; if (!midend_process_key(fe->me, event->x - fe->ox, @@ -1306,22 +1350,22 @@ static gint configure_area(GtkWidget *widget, { frontend *fe = (frontend *)data; int x, y; + int oldw = fe->w, oldpw = fe->pw, oldh = fe->h, oldph = fe->ph; x = event->width; y = event->height; - - if (x != fe->w || y != fe->h || !backing_store_ok(fe)) { + fe->w = x; + fe->h = y; + midend_size(fe->me, &x, &y, TRUE); + fe->pw = x; + fe->ph = y; + fe->ox = (fe->w - fe->pw) / 2; + fe->oy = (fe->h - fe->ph) / 2; + + if (oldw != fe->w || oldpw != fe->pw || + oldh != fe->h || oldph != fe->ph || !backing_store_ok(fe)) { if (backing_store_ok(fe)) teardown_backing_store(fe); - - fe->w = x; - fe->h = y; - midend_size(fe->me, &x, &y, TRUE); - fe->pw = x; - fe->ph = y; - fe->ox = (fe->w - fe->pw) / 2; - fe->oy = (fe->h - fe->ph) / 2; - setup_backing_store(fe); } @@ -1372,18 +1416,6 @@ static void window_destroy(GtkWidget *widget, gpointer data) gtk_main_quit(); } -static void msgbox_button_clicked(GtkButton *button, gpointer data) -{ - GtkWidget *window = GTK_WIDGET(data); - int v, *ip; - - ip = (int *)g_object_get_data(G_OBJECT(window), "user-data"); - v = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "user-data")); - *ip = v; - - gtk_widget_destroy(GTK_WIDGET(data)); -} - static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) { GObject *cancelbutton = G_OBJECT(data); @@ -1401,6 +1433,54 @@ static int win_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) enum { MB_OK, MB_YESNO }; +static void align_label(GtkLabel *label, double x, double y) +{ +#if GTK_CHECK_VERSION(3,16,0) + gtk_label_set_xalign(label, x); + gtk_label_set_yalign(label, y); +#elif GTK_CHECK_VERSION(3,14,0) + gtk_widget_set_halign(GTK_WIDGET(label), + x == 0 ? GTK_ALIGN_START : + x == 1 ? GTK_ALIGN_END : GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(label), + y == 0 ? GTK_ALIGN_START : + y == 1 ? GTK_ALIGN_END : GTK_ALIGN_CENTER); +#else + gtk_misc_set_alignment(GTK_MISC(label), x, y); +#endif +} + +#if GTK_CHECK_VERSION(3,0,0) +int message_box(GtkWidget *parent, char *title, char *msg, int centre, + int type) +{ + GtkWidget *window; + gint ret; + + window = gtk_message_dialog_new + (GTK_WINDOW(parent), + (GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), + (type == MB_OK ? GTK_MESSAGE_INFO : GTK_MESSAGE_QUESTION), + (type == MB_OK ? GTK_BUTTONS_OK : GTK_BUTTONS_YES_NO), + "%s", msg); + gtk_window_set_title(GTK_WINDOW(window), title); + ret = gtk_dialog_run(GTK_DIALOG(window)); + gtk_widget_destroy(window); + return (type == MB_OK ? TRUE : (ret == GTK_RESPONSE_YES)); +} +#else /* GTK_CHECK_VERSION(3,0,0) */ +static void msgbox_button_clicked(GtkButton *button, gpointer data) +{ + GtkWidget *window = GTK_WIDGET(data); + int v, *ip; + + ip = (int *)g_object_get_data(G_OBJECT(window), "user-data"); + v = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "user-data")); + *ip = v; + + gtk_widget_destroy(GTK_WIDGET(data)); +} + int message_box(GtkWidget *parent, char *title, char *msg, int centre, int type) { @@ -1410,7 +1490,7 @@ int message_box(GtkWidget *parent, char *title, char *msg, int centre, window = gtk_dialog_new(); text = gtk_label_new(msg); - gtk_misc_set_alignment(GTK_MISC(text), 0.0, 0.0); + align_label(GTK_LABEL(text), 0.0, 0.0); hbox = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), text, FALSE, FALSE, 20); gtk_box_pack_start @@ -1464,6 +1544,7 @@ int message_box(GtkWidget *parent, char *title, char *msg, int centre, gtk_main(); return (type == MB_YESNO ? i == 1 : TRUE); } +#endif /* GTK_CHECK_VERSION(3,0,0) */ void error_box(GtkWidget *parent, char *msg) { @@ -1540,6 +1621,7 @@ static void droplist_sel(GtkComboBox *combo, gpointer data) static int get_config(frontend *fe, int which) { GtkWidget *w, *table, *cancel; + GtkBox *content_box, *button_box; char *title; config_item *i; int y; @@ -1548,23 +1630,38 @@ static int get_config(frontend *fe, int which) fe->cfg_which = which; fe->cfgret = FALSE; +#if GTK_CHECK_VERSION(3,0,0) + /* GtkDialog isn't quite flexible enough */ + fe->cfgbox = gtk_window_new(GTK_WINDOW_TOPLEVEL); + content_box = GTK_BOX(gtk_vbox_new(FALSE, 8)); + g_object_set(G_OBJECT(content_box), "margin", 8, (const char *)NULL); + gtk_widget_show(GTK_WIDGET(content_box)); + gtk_container_add(GTK_CONTAINER(fe->cfgbox), GTK_WIDGET(content_box)); + button_box = GTK_BOX(gtk_hbox_new(FALSE, 8)); + gtk_widget_show(GTK_WIDGET(button_box)); + gtk_box_pack_end(content_box, GTK_WIDGET(button_box), FALSE, FALSE, 0); + { + GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + gtk_widget_show(sep); + gtk_box_pack_end(content_box, sep, FALSE, FALSE, 0); + } +#else fe->cfgbox = gtk_dialog_new(); + content_box = GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(fe->cfgbox))); + button_box = GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(fe->cfgbox))); +#endif gtk_window_set_title(GTK_WINDOW(fe->cfgbox), title); sfree(title); w = gtk_button_new_with_our_label(LABEL_CANCEL); - gtk_box_pack_end - (GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(fe->cfgbox))), - w, FALSE, FALSE, 0); + gtk_box_pack_end(button_box, w, FALSE, FALSE, 0); gtk_widget_show(w); g_signal_connect(G_OBJECT(w), "clicked", G_CALLBACK(config_cancel_button_clicked), fe); cancel = w; w = gtk_button_new_with_our_label(LABEL_OK); - gtk_box_pack_end - (GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(fe->cfgbox))), - w, FALSE, FALSE, 0); + gtk_box_pack_end(button_box, w, FALSE, FALSE, 0); gtk_widget_show(w); gtk_widget_set_can_default(w, TRUE); gtk_window_set_default(GTK_WINDOW(fe->cfgbox), w); @@ -1577,9 +1674,7 @@ static int get_config(frontend *fe, int which) table = gtk_table_new(1, 2, FALSE); #endif y = 0; - gtk_box_pack_start - (GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(fe->cfgbox))), - table, FALSE, FALSE, 0); + gtk_box_pack_start(content_box, table, FALSE, FALSE, 0); gtk_widget_show(table); for (i = fe->cfg; i->type != C_END; i++) { @@ -1594,7 +1689,7 @@ static int get_config(frontend *fe, int which) */ w = gtk_label_new(i->name); - gtk_misc_set_alignment(GTK_MISC(w), 0.0, 0.5); + align_label(GTK_LABEL(w), 0.0, 0.5); #if GTK_CHECK_VERSION(3,0,0) gtk_grid_attach(GTK_GRID(table), w, 0, y, 1, 1); #else @@ -1650,7 +1745,7 @@ static int get_config(frontend *fe, int which) */ w = gtk_label_new(i->name); - gtk_misc_set_alignment(GTK_MISC(w), 0.0, 0.5); + align_label(GTK_LABEL(w), 0.0, 0.5); #if GTK_CHECK_VERSION(3,0,0) gtk_grid_attach(GTK_GRID(table), w, 0, y, 1, 1); #else @@ -1787,20 +1882,22 @@ static void changed_preset(frontend *fe) TRUE); } else { GSList *gs = fe->preset_radio; - int i = fe->n_preset_menu_items - 1 - n; - if (fe->preset_custom) - gs = gs->next; - while (i && gs) { - i--; - gs = gs->next; - } - if (gs) { - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gs->data), - TRUE); - } else for (gs = fe->preset_radio; gs; gs = gs->next) { - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gs->data), - FALSE); - } + GSList *found = NULL; + + for (gs = fe->preset_radio; gs; gs = gs->next) { + struct preset_menu_entry *entry = + (struct preset_menu_entry *)g_object_get_data( + G_OBJECT(gs->data), "user-data"); + + if (entry && entry->id != n) + gtk_check_menu_item_set_active( + GTK_CHECK_MENU_ITEM(gs->data), FALSE); + else + found = gs; + } + if (found) + gtk_check_menu_item_set_active( + GTK_CHECK_MENU_ITEM(found->data), FALSE); } fe->preset_threaded = FALSE; @@ -1880,6 +1977,24 @@ static gint configure_window(GtkWidget *widget, return FALSE; } +#if GTK_CHECK_VERSION(3,0,0) +static int window_extra_height(frontend *fe) +{ + int ret = 0; + if (fe->menubar) { + GtkRequisition req; + gtk_widget_get_preferred_size(fe->menubar, &req, NULL); + ret += req.height; + } + if (fe->statusbar) { + GtkRequisition req; + gtk_widget_get_preferred_size(fe->statusbar, &req, NULL); + ret += req.height; + } + return ret; +} +#endif + static void resize_fe(frontend *fe) { int x, y; @@ -1887,10 +2002,8 @@ static void resize_fe(frontend *fe) get_size(fe, &x, &y); #if GTK_CHECK_VERSION(3,0,0) - gtk_window_resize_to_geometry(GTK_WINDOW(fe->window), x, y); + gtk_window_resize(GTK_WINDOW(fe->window), x, y + window_extra_height(fe)); #else - fe->w = x; - fe->h = y; fe->drawing_area_shrink_pending = FALSE; gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y); { @@ -1906,17 +2019,19 @@ static void resize_fe(frontend *fe) static void menu_preset_event(GtkMenuItem *menuitem, gpointer data) { frontend *fe = (frontend *)data; - game_params *params = - (game_params *)g_object_get_data(G_OBJECT(menuitem), "user-data"); + struct preset_menu_entry *entry = + (struct preset_menu_entry *)g_object_get_data( + G_OBJECT(menuitem), "user-data"); if (fe->preset_threaded || (GTK_IS_CHECK_MENU_ITEM(menuitem) && !gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem)))) return; - midend_set_params(fe->me, params); + midend_set_params(fe->me, entry->params); midend_new_game(fe->me); changed_preset(fe); resize_fe(fe); + midend_redraw(fe->me); } GdkAtom compound_text_atom, utf8_string_atom; @@ -2052,8 +2167,9 @@ static char *file_selector(frontend *fe, char *title, int save) NULL); if (gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) { - const char *name = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel)); + char *name = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel)); filesel_name = dupstr(name); + g_free(name); } gtk_widget_destroy(filesel); @@ -2103,15 +2219,14 @@ static void menu_save_event(GtkMenuItem *menuitem, gpointer data) " file \"%.*s\"?", FILENAME_MAX, name); if (!message_box(fe->window, "Question", buf, TRUE, MB_YESNO)) - return; + goto free_and_return; } fp = fopen(name, "w"); - sfree(name); if (!fp) { error_box(fe->window, "Unable to open save file"); - return; + goto free_and_return; } { @@ -2125,10 +2240,11 @@ static void menu_save_event(GtkMenuItem *menuitem, gpointer data) sprintf(boxmsg, "Error writing save file: %.400s", strerror(errno)); error_box(fe->window, boxmsg); - return; + goto free_and_return; } } - + free_and_return: + sfree(name); } } @@ -2159,6 +2275,7 @@ static void menu_load_event(GtkMenuItem *menuitem, gpointer data) changed_preset(fe); resize_fe(fe); + midend_redraw(fe->me); } } @@ -2196,11 +2313,110 @@ static void menu_config_event(GtkMenuItem *menuitem, gpointer data) midend_new_game(fe->me); resize_fe(fe); + midend_redraw(fe->me); +} + +#ifndef HELP_BROWSER_PATH +#define HELP_BROWSER_PATH "xdg-open:sensible-browser" +#endif + +static void show_help(frontend *fe, const char *topic) +{ + const char *list = HELP_BROWSER_PATH; + char path[PATH_MAX + 1]; + struct { + const char *s; + int len; + } lang[3]; + int i; + + /* + * Search for help file, trying: + * 1. Version for this locale, ignoring encoding (HTML browsers + * must handle multiple encodings) + * 2. Version for this locale, ignoring encoding and country + * 3. English version + */ + lang[0].s = setlocale(LC_MESSAGES, NULL); + lang[0].len = strcspn(lang[0].s, ".@"); + lang[1].s = lang[0].s; + lang[1].len = strcspn(lang[1].s, "_"); + if (lang[1].len > lang[0].len) + lang[1].len = lang[0].len; + lang[2].s = "en"; + lang[2].len = 2; + for (i = 0; i < lenof(lang); i++) { + sprintf(path, "%s/sgt-puzzles/help/%.*s/%s.html", + SHAREDIR, lang[i].len, lang[i].s, topic); + if (access(path, R_OK) == 0) + break; + } + if (i == lenof(lang)) { + error_box(fe->window, "Help file is not installed"); + return; + } + + for (;;) { + size_t len; + char buf[PATH_MAX + 1]; + const char *command; + const char *argv[3]; + + len = strcspn(list, ":"); + if (len <= PATH_MAX) { + memcpy(buf, list, len); + buf[len] = 0; + if (buf[0] == '$') + command = getenv(buf + 1); + else + command = buf; + if (command) { + argv[0] = command; + argv[1] = path; + argv[2] = NULL; + if (g_spawn_async(NULL, (char **)argv, NULL, + G_SPAWN_SEARCH_PATH, + NULL, NULL, NULL, NULL)) + return; + } + } + + if (!list[len]) + break; + list += len + 1; + } + + error_box(fe->window, "Failed to start a help browser"); +} + +static void menu_help_contents_event(GtkMenuItem *menuitem, gpointer data) +{ + show_help((frontend *)data, "index"); +} + +static void menu_help_specific_event(GtkMenuItem *menuitem, gpointer data) +{ + show_help((frontend *)data, thegame.htmlhelp_topic); } static void menu_about_event(GtkMenuItem *menuitem, gpointer data) { frontend *fe = (frontend *)data; + +#if GTK_CHECK_VERSION(3,0,0) + extern char *const *const xpm_icons[]; + extern const int n_xpm_icons; + GdkPixbuf *icon = gdk_pixbuf_new_from_xpm_data + ((const gchar **)xpm_icons[n_xpm_icons-1]); + gtk_show_about_dialog + (GTK_WINDOW(fe->window), + "program-name", thegame.name, + "version", ver, + "comments", "Part of Simon Tatham's Portable Puzzle Collection", + "logo", icon, + (const gchar *)NULL); + g_object_unref(G_OBJECT(icon)); +#else char titlebuf[256]; char textbuf[1024]; @@ -2211,34 +2427,37 @@ static void menu_about_event(GtkMenuItem *menuitem, gpointer data) "%.500s", thegame.name, ver); message_box(fe->window, titlebuf, textbuf, TRUE, MB_OK); +#endif } -static GtkWidget *add_menu_item_with_key(frontend *fe, GtkContainer *cont, - char *text, int key) +static GtkWidget *add_menu_ui_item( + frontend *fe, GtkContainer *cont, char *text, int action, + int accel_key, int accel_keyqual) { GtkWidget *menuitem = gtk_menu_item_new_with_label(text); - int keyqual; gtk_container_add(cont, menuitem); - g_object_set_data(G_OBJECT(menuitem), "user-data", GINT_TO_POINTER(key)); + g_object_set_data(G_OBJECT(menuitem), "user-data", + GINT_TO_POINTER(action)); g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menu_key_event), fe); - switch (key & ~0x1F) { - case 0x00: - key += 0x60; - keyqual = GDK_CONTROL_MASK; - break; - case 0x40: - key += 0x20; - keyqual = GDK_SHIFT_MASK; - break; - default: - keyqual = 0; - break; + + if (accel_key) { + /* + * Display a keyboard accelerator alongside this menu item. + * Actually this won't be processed via the usual GTK + * accelerator system, because we add it to a dummy + * accelerator group which is never actually activated on the + * main window; this permits back ends to override special + * keys like 'n' and 'r' and 'u' in some UI states. So + * whatever keystroke we display here will still go to + * key_event and be handled in the normal way. + */ + gtk_widget_add_accelerator(menuitem, + "activate", fe->dummy_accelgroup, + accel_key, accel_keyqual, + GTK_ACCEL_VISIBLE | GTK_ACCEL_LOCKED); } - gtk_widget_add_accelerator(menuitem, - "activate", fe->accelgroup, - key, keyqual, - GTK_ACCEL_VISIBLE); + gtk_widget_show(menuitem); return menuitem; } @@ -2250,6 +2469,36 @@ static void add_menu_separator(GtkContainer *cont) gtk_widget_show(menuitem); } +static void populate_gtk_preset_menu(frontend *fe, struct preset_menu *menu, + GtkWidget *gtkmenu) +{ + int i; + + for (i = 0; i < menu->n_entries; i++) { + struct preset_menu_entry *entry = &menu->entries[i]; + GtkWidget *menuitem; + + if (entry->params) { + menuitem = gtk_radio_menu_item_new_with_label( + fe->preset_radio, entry->title); + fe->preset_radio = gtk_radio_menu_item_get_group( + GTK_RADIO_MENU_ITEM(menuitem)); + g_object_set_data(G_OBJECT(menuitem), "user-data", entry); + g_signal_connect(G_OBJECT(menuitem), "activate", + G_CALLBACK(menu_preset_event), fe); + } else { + GtkWidget *submenu; + menuitem = gtk_menu_item_new_with_label(entry->title); + submenu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu); + populate_gtk_preset_menu(fe, entry->submenu, submenu); + } + + gtk_container_add(GTK_CONTAINER(gtkmenu), menuitem); + gtk_widget_show(menuitem); + } +} + enum { ARG_EITHER, ARG_SAVE, ARG_ID }; /* for argtype */ static frontend *new_window(char *arg, int argtype, char **error) @@ -2262,8 +2511,12 @@ static frontend *new_window(char *arg, int argtype, char **error) char errbuf[1024]; extern char *const *const xpm_icons[]; extern const int n_xpm_icons; + struct preset_menu *preset_menu; fe = snew(frontend); +#if GTK_CHECK_VERSION(3,20,0) + fe->css_provider = NULL; +#endif fe->timer_active = FALSE; fe->timer_id = -1; @@ -2366,8 +2619,11 @@ static frontend *new_window(char *arg, int argtype, char **error) gtk_container_add(GTK_CONTAINER(fe->window), GTK_WIDGET(vbox)); gtk_widget_show(GTK_WIDGET(vbox)); - fe->accelgroup = gtk_accel_group_new(); - gtk_window_add_accel_group(GTK_WINDOW(fe->window), fe->accelgroup); + fe->dummy_accelgroup = gtk_accel_group_new(); + /* + * Intentionally _not_ added to the window via + * gtk_window_add_accel_group; see menu_key_event + */ hbox = GTK_BOX(gtk_hbox_new(FALSE, 0)); gtk_box_pack_start(vbox, GTK_WIDGET(hbox), FALSE, FALSE, 0); @@ -2384,7 +2640,7 @@ static frontend *new_window(char *arg, int argtype, char **error) menu = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu); - add_menu_item_with_key(fe, GTK_CONTAINER(menu), "New", 'n'); + add_menu_ui_item(fe, GTK_CONTAINER(menu), "New", UI_NEWGAME, 'n', 0); menuitem = gtk_menu_item_new_with_label("Restart"); gtk_container_add(GTK_CONTAINER(menu), menuitem); @@ -2410,11 +2666,11 @@ static frontend *new_window(char *arg, int argtype, char **error) fe->preset_radio = NULL; fe->preset_custom = NULL; - fe->n_preset_menu_items = 0; fe->preset_threaded = FALSE; - if ((n = midend_num_presets(fe->me)) > 0 || thegame.can_configure) { + + preset_menu = midend_get_presets(fe->me, NULL); + if (preset_menu->n_entries > 0 || thegame.can_configure) { GtkWidget *submenu; - int i; menuitem = gtk_menu_item_new_with_mnemonic("_Type"); gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem); @@ -2423,23 +2679,7 @@ static frontend *new_window(char *arg, int argtype, char **error) submenu = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu); - for (i = 0; i < n; i++) { - char *name; - game_params *params; - - midend_fetch_preset(fe->me, i, &name, ¶ms); - - menuitem = - gtk_radio_menu_item_new_with_label(fe->preset_radio, name); - fe->preset_radio = - gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem)); - fe->n_preset_menu_items++; - gtk_container_add(GTK_CONTAINER(submenu), menuitem); - g_object_set_data(G_OBJECT(menuitem), "user-data", params); - g_signal_connect(G_OBJECT(menuitem), "activate", - G_CALLBACK(menu_preset_event), fe); - gtk_widget_show(menuitem); - } + populate_gtk_preset_menu(fe, preset_menu, submenu); if (thegame.can_configure) { menuitem = fe->preset_custom = @@ -2470,8 +2710,8 @@ static frontend *new_window(char *arg, int argtype, char **error) gtk_widget_show(menuitem); #ifndef STYLUS_BASED add_menu_separator(GTK_CONTAINER(menu)); - add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Undo", 'u'); - add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Redo", 'r'); + add_menu_ui_item(fe, GTK_CONTAINER(menu), "Undo", UI_UNDO, 'u', 0); + add_menu_ui_item(fe, GTK_CONTAINER(menu), "Redo", UI_REDO, 'r', 0); #endif if (thegame.can_format_as_text_ever) { add_menu_separator(GTK_CONTAINER(menu)); @@ -2493,7 +2733,7 @@ static frontend *new_window(char *arg, int argtype, char **error) gtk_widget_show(menuitem); } add_menu_separator(GTK_CONTAINER(menu)); - add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Exit", 'q'); + add_menu_ui_item(fe, GTK_CONTAINER(menu), "Exit", UI_QUIT, 'q', 0); menuitem = gtk_menu_item_new_with_mnemonic("_Help"); gtk_container_add(GTK_CONTAINER(fe->menubar), menuitem); @@ -2502,6 +2742,25 @@ static frontend *new_window(char *arg, int argtype, char **error) menu = gtk_menu_new(); gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu); + menuitem = gtk_menu_item_new_with_label("Contents"); + gtk_container_add(GTK_CONTAINER(menu), menuitem); + g_signal_connect(G_OBJECT(menuitem), "activate", + G_CALLBACK(menu_help_contents_event), fe); + gtk_widget_show(menuitem); + + if (thegame.htmlhelp_topic) { + char *item; + assert(thegame.name); + item = snewn(9+strlen(thegame.name), char); /*ick*/ + sprintf(item, "Help on %s", thegame.name); + menuitem = gtk_menu_item_new_with_label(item); + sfree(item); + gtk_container_add(GTK_CONTAINER(menu), menuitem); + g_signal_connect(G_OBJECT(menuitem), "activate", + G_CALLBACK(menu_help_specific_event), fe); + gtk_widget_show(menuitem); + } + menuitem = gtk_menu_item_new_with_label("About"); gtk_container_add(GTK_CONTAINER(menu), menuitem); g_signal_connect(G_OBJECT(menuitem), "activate", @@ -2511,7 +2770,7 @@ static frontend *new_window(char *arg, int argtype, char **error) #ifdef STYLUS_BASED menuitem=gtk_button_new_with_mnemonic("_Redo"); g_object_set_data(G_OBJECT(menuitem), "user-data", - GINT_TO_POINTER((int)('r'))); + GINT_TO_POINTER(UI_REDO)); g_signal_connect(G_OBJECT(menuitem), "clicked", G_CALLBACK(menu_key_event), fe); gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0); @@ -2519,7 +2778,7 @@ static frontend *new_window(char *arg, int argtype, char **error) menuitem=gtk_button_new_with_mnemonic("_Undo"); g_object_set_data(G_OBJECT(menuitem), "user-data", - GINT_TO_POINTER((int)('u'))); + GINT_TO_POINTER(UI_UNDO)); g_signal_connect(G_OBJECT(menuitem), "clicked", G_CALLBACK(menu_key_event), fe); gtk_box_pack_end(hbox, menuitem, FALSE, FALSE, 0); @@ -2562,7 +2821,7 @@ static frontend *new_window(char *arg, int argtype, char **error) fe->statusctx = gtk_statusbar_get_context_id (GTK_STATUSBAR(fe->statusbar), "game"); gtk_statusbar_push(GTK_STATUSBAR(fe->statusbar), fe->statusctx, - "test"); + DEFAULT_STATUSBAR_TEXT); #if GTK_CHECK_VERSION(3,0,0) gtk_widget_get_preferred_size(fe->statusbar, &req, NULL); #else @@ -2578,19 +2837,27 @@ static frontend *new_window(char *arg, int argtype, char **error) #endif { GdkGeometry geom; - geom.base_width = geom.base_height = 0; + geom.base_width = 0; +#if GTK_CHECK_VERSION(3,0,0) + geom.base_height = window_extra_height(fe); + gtk_window_set_geometry_hints(GTK_WINDOW(fe->window), NULL, + &geom, GDK_HINT_BASE_SIZE); +#else + geom.base_height = 0; gtk_window_set_geometry_hints(GTK_WINDOW(fe->window), fe->area, &geom, GDK_HINT_BASE_SIZE); +#endif } + fe->w = -1; + fe->h = -1; get_size(fe, &x, &y); #if GTK_CHECK_VERSION(3,0,0) - gtk_window_set_default_geometry(GTK_WINDOW(fe->window), x, y); + gtk_window_set_default_size(GTK_WINDOW(fe->window), + x, y + window_extra_height(fe)); #else fe->drawing_area_shrink_pending = FALSE; gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y); #endif - fe->w = x; - fe->h = y; gtk_box_pack_end(vbox, fe->area, TRUE, TRUE, 0); @@ -2682,6 +2949,22 @@ char *fgetline(FILE *fp) return ret; } +static void list_presets_from_menu(struct preset_menu *menu) +{ + int i; + + for (i = 0; i < menu->n_entries; i++) { + if (menu->entries[i].params) { + char *paramstr = thegame.encode_params( + menu->entries[i].params, TRUE); + printf("%s %s\n", paramstr, menu->entries[i].title); + sfree(paramstr); + } else { + list_presets_from_menu(menu->entries[i].submenu); + } + } +} + int main(int argc, char **argv) { char *pname = argv[0]; @@ -3085,23 +3368,12 @@ int main(int argc, char **argv) * Another specialist mode which causes the puzzle to list the * game_params strings for all its preset configurations. */ - int i, npresets; midend *me; + struct preset_menu *menu; me = midend_new(NULL, &thegame, NULL, NULL); - npresets = midend_num_presets(me); - - for (i = 0; i < npresets; i++) { - game_params *params; - char *name, *paramstr; - - midend_fetch_preset(me, i, &name, ¶ms); - paramstr = thegame.encode_params(params, TRUE); - - printf("%s %s\n", paramstr, name); - sfree(paramstr); - } - + menu = midend_get_presets(me, NULL); + list_presets_from_menu(menu); midend_free(me); return 0; } else {