From a7dc17c4258837b0ee3927f1db5e1c02acee5cc3 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 24 Apr 2017 16:00:24 +0100 Subject: [PATCH] Rework the preset menu system to permit submenus. To do this, I've completely replaced the API between mid-end and front end, so any downstream front end maintainers will have to do some rewriting of their own (sorry). I've done the necessary work in all five of the front ends I keep in-tree here - Windows, GTK, OS X, Javascript/Emscripten, and Java/NestedVM - and I've done it in various different styles (as each front end found most convenient), so that should provide a variety of sample code to show downstreams how, if they should need it. I've left in the old puzzle back-end API function to return a flat list of presets, so for the moment, all the puzzle backends are unchanged apart from an extra null pointer appearing in their top-level game structure. In a future commit I'll actually use the new feature in a puzzle; perhaps in the further future it might make sense to migrate all the puzzles to the new API and stop providing back ends with two alternative ways of doing things, but this seemed like enough upheaval for one day. --- PuzzleApplet.java | 61 +++++++-- blackbox.c | 2 +- bridges.c | 2 +- cube.c | 2 +- devel.but | 170 ++++++++++++++++++++---- dominosa.c | 2 +- emcc.c | 41 +++--- emcclib.js | 59 ++++++--- emccpre.js | 4 +- fifteen.c | 2 +- filling.c | 2 +- flip.c | 2 +- flood.c | 2 +- galaxies.c | 2 +- gtk.c | 126 ++++++++++-------- guess.c | 2 +- inertia.c | 2 +- keen.c | 2 +- lightup.c | 2 +- loopy.c | 2 +- magnets.c | 2 +- map.c | 2 +- midend.c | 291 +++++++++++++++++++++++++++++------------- mines.c | 2 +- nestedvm.c | 31 +++-- net.c | 2 +- netslide.c | 2 +- nullfe.c | 5 + nullgame.c | 2 +- osx.m | 109 +++++++++++----- palisade.c | 2 +- pattern.c | 2 +- pearl.c | 2 +- pegs.c | 2 +- puzzles.h | 53 +++++++- range.c | 2 +- rect.c | 2 +- samegame.c | 2 +- signpost.c | 2 +- singles.c | 2 +- sixteen.c | 2 +- slant.c | 2 +- solo.c | 2 +- tents.c | 2 +- towers.c | 2 +- tracks.c | 2 +- twiddle.c | 2 +- undead.c | 2 +- unequal.c | 2 +- unfinished/group.c | 2 +- unfinished/separate.c | 2 +- unfinished/slide.c | 2 +- unfinished/sokoban.c | 2 +- unruly.c | 2 +- untangle.c | 2 +- windows.c | 122 ++++++++++++------ 56 files changed, 814 insertions(+), 346 deletions(-) diff --git a/PuzzleApplet.java b/PuzzleApplet.java index 0b0648c..512aede 100644 --- a/PuzzleApplet.java +++ b/PuzzleApplet.java @@ -28,6 +28,9 @@ public class PuzzleApplet extends JApplet implements Runtime.CallJavaCB { private JFrame mainWindow; private JMenu typeMenu; + private JMenuItem[] typeMenuItems; + private int customMenuItemIndex; + private JMenuItem solveCommand; private Color[] colors; private JLabel statusBar; @@ -219,17 +222,17 @@ public class PuzzleApplet extends JApplet implements Runtime.CallJavaCB { } private JMenuItem addMenuItemCallback(JMenu jm, String name, final String callback, final int arg) { - return addMenuItemCallback(jm, name, callback, new int[] {arg}); + return addMenuItemCallback(jm, name, callback, new int[] {arg}, false); } private JMenuItem addMenuItemCallback(JMenu jm, String name, final String callback) { - return addMenuItemCallback(jm, name, callback, new int[0]); + return addMenuItemCallback(jm, name, callback, new int[0], false); } - private JMenuItem addMenuItemCallback(JMenu jm, String name, final String callback, final int[] args) { + private JMenuItem addMenuItemCallback(JMenu jm, String name, final String callback, final int[] args, boolean checkbox) { JMenuItem jmi; - if (jm == typeMenu) - typeMenu.add(jmi = new JCheckBoxMenuItem(name)); + if (checkbox) + jm.add(jmi = new JCheckBoxMenuItem(name)); else jm.add(jmi = new JMenuItem(name)); jmi.addActionListener(new ActionListener() { @@ -261,12 +264,29 @@ public class PuzzleApplet extends JApplet implements Runtime.CallJavaCB { } else { typeMenu.setVisible(true); } - addMenuItemCallback(typeMenu, "Custom...", "jcallback_config_event", CFG_SETTINGS); + typeMenuItems[customMenuItemIndex] = + addMenuItemCallback(typeMenu, "Custom...", + "jcallback_config_event", + new int[] {CFG_SETTINGS}, true); } - private void addTypeItem(String name, final int ptrGameParams) { + private void addTypeItem + (JMenu targetMenu, String name, int newId, final int ptrGameParams) { + typeMenu.setVisible(true); - addMenuItemCallback(typeMenu, name, "jcallback_preset_event", ptrGameParams); + typeMenuItems[newId] = + addMenuItemCallback(targetMenu, name, + "jcallback_preset_event", + new int[] {ptrGameParams}, true); + } + + private void addTypeSubmenu + (JMenu targetMenu, String name, int newId) { + + JMenu newMenu = new JMenu(name); + newMenu.setVisible(true); + typeMenuItems[newId] = newMenu; + targetMenu.add(newMenu); } public int call(int cmd, int arg1, int arg2, int arg3) { @@ -279,8 +299,20 @@ public class PuzzleApplet extends JApplet implements Runtime.CallJavaCB { if ((arg2 & 4) != 0) solveCommand.setEnabled(true); colors = new Color[arg3]; return 0; - case 1: // Type menu item - addTypeItem(runtime.cstring(arg1), arg2); + case 1: // configure Type menu + if (arg1 == 0) { + // preliminary setup + typeMenuItems = new JMenuItem[arg2 + 2]; + typeMenuItems[arg2] = typeMenu; + customMenuItemIndex = arg2 + 1; + return arg2; + } else if (xarg1 != 0) { + addTypeItem((JMenu)typeMenuItems[arg2], + runtime.cstring(arg1), arg3, xarg1); + } else { + addTypeSubmenu((JMenu)typeMenuItems[arg2], + runtime.cstring(arg1), arg3); + } return 0; case 2: // MessageBox JOptionPane.showMessageDialog(this, runtime.cstring(arg2), runtime.cstring(arg1), arg3 == 0 ? JOptionPane.INFORMATION_MESSAGE : JOptionPane.ERROR_MESSAGE); @@ -432,10 +464,11 @@ public class PuzzleApplet extends JApplet implements Runtime.CallJavaCB { dlg = null; return 0; case 13: // tick a menu item - if (arg1 < 0) arg1 = typeMenu.getItemCount() - 1; - for (int i = 0; i < typeMenu.getItemCount(); i++) { - if (typeMenu.getMenuComponent(i) instanceof JCheckBoxMenuItem) { - ((JCheckBoxMenuItem)typeMenu.getMenuComponent(i)).setSelected(arg1 == i); + if (arg1 < 0) arg1 = customMenuItemIndex; + for (int i = 0; i < typeMenuItems.length; i++) { + if (typeMenuItems[i] instanceof JCheckBoxMenuItem) { + ((JCheckBoxMenuItem)typeMenuItems[i]).setSelected + (arg1 == i); } } return 0; diff --git a/blackbox.c b/blackbox.c index 629b7ec..b334cf7 100644 --- a/blackbox.c +++ b/blackbox.c @@ -1505,7 +1505,7 @@ static void game_print(drawing *dr, const game_state *state, int tilesize) const struct game thegame = { "Black Box", "games.blackbox", "blackbox", default_params, - game_fetch_preset, + game_fetch_preset, NULL, decode_params, encode_params, free_params, diff --git a/bridges.c b/bridges.c index de7403e..6975208 100644 --- a/bridges.c +++ b/bridges.c @@ -3224,7 +3224,7 @@ static void game_print(drawing *dr, const game_state *state, int ts) const struct game thegame = { "Bridges", "games.bridges", "bridges", default_params, - game_fetch_preset, + game_fetch_preset, NULL, decode_params, encode_params, free_params, diff --git a/cube.c b/cube.c index c22e299..a30dc10 100644 --- a/cube.c +++ b/cube.c @@ -1737,7 +1737,7 @@ static void game_print(drawing *dr, const game_state *state, int tilesize) const struct game thegame = { "Cube", "games.cube", "cube", default_params, - game_fetch_preset, + game_fetch_preset, NULL, decode_params, encode_params, free_params, diff --git a/devel.but b/devel.but index 9befcad..a38fdda 100644 --- a/devel.but +++ b/devel.but @@ -391,8 +391,9 @@ with the default values, and returns a pointer to it. \c int (*fetch_preset)(int i, char **name, game_params **params); -This function is used to populate the \q{Type} menu, which provides -a list of conveniently accessible preset parameters for most games. +This function is one of the two APIs a back end can provide to +populate the \q{Type} menu, which provides a list of conveniently +accessible preset parameters for most games. The function is called with \c{i} equal to the index of the preset required (numbering from zero). It returns \cw{FALSE} if that preset @@ -406,6 +407,33 @@ returns \cw{TRUE}. If the game does not wish to support any presets at all, this function is permitted to return \cw{FALSE} always. +If the game wants to return presets in the form of a hierarchical menu +instead of a flat list (and, indeed, even if it doesn't), then it may +set this function pointer to \cw{NULL}, and instead fill in the +alternative function pointer \cw{preset_menu} +(\k{backend-preset-menu}). + +\S{backend-preset-menu} \cw{preset_menu()} + +\c struct preset_menu *(*preset_menu)(void); + +This function is the more flexible of the two APIs by which a back end +can define a collection of preset game parameters. + +This function simply returns a complete menu hierarchy, in the form of +a \c{struct preset_menu} (see \k{midend-get-presets}) and further +submenus (if it wishes) dangling off it. There are utility functions +described in \k{utils-presets} to make it easy for the back end to +construct this menu. + +If the game has no need to return a hierarchy of menus, it may instead +opt to implement the \cw{fetch_preset()} function (see +\k{backend-fetch-preset}). + +The game need not fill in the \c{id} fields in the preset menu +structures. The mid-end will do that after it receives the structure +from the game, and before passing it on to the front end. + \S{backend-encode-params} \cw{encode_params()} \c char *(*encode_params)(const game_params *params, int full); @@ -2743,8 +2771,8 @@ these parameters until further notice. The usual way in which the front end will have an actual \c{game_params} structure to pass to this function is if it had -previously got it from \cw{midend_fetch_preset()} -(\k{midend-fetch-preset}). Thus, this function is usually called in +previously got it from \cw{midend_get_presets()} +(\k{midend-get-presets}). Thus, this function is usually called in response to the user making a selection from the presets menu. \H{midend-get-params} \cw{midend_get_params()} @@ -2966,34 +2994,63 @@ One of the major purposes of timing in the mid-end is to perform move animation. Therefore, calling this function is very likely to result in calls back to the front end's drawing API. -\H{midend-num-presets} \cw{midend_num_presets()} +\H{midend-get-presets} \cw{midend_get_presets()} -\c int midend_num_presets(midend *me); +\c struct preset_menu *midend_get_presets(midend *me, int *id_limit); -Returns the number of game parameter presets supplied by this game. -Front ends should use this function and \cw{midend_fetch_preset()} -to configure their presets menu rather than calling the back end -directly, since the mid-end adds standard customisation facilities. -(At the time of writing, those customisation facilities are -implemented hackily by means of environment variables, but it's not -impossible that they may become more full and formal in future.) +Returns a data structure describing this game's collection of preset +game parameters, organised into a hierarchical structure of menus and +submenus. -\H{midend-fetch-preset} \cw{midend_fetch_preset()} +The return value is a pointer to a data structure containing the +following fields (among others, which are not intended for front end +use): -\c void midend_fetch_preset(midend *me, int n, -\c char **name, game_params **params); +\c struct preset_menu { +\c int n_entries; +\c struct preset_menu_entry *entries; +\c /* and other things */ +\e iiiiiiiiiiiiiiiiiiiiii +\c }; -Returns one of the preset game parameter structures for the game. On -input \c{n} must be a non-negative integer and less than the value -returned from \cw{midend_num_presets()}. On output, \c{*name} is set -to an ASCII string suitable for entering in the game's presets menu, -and \c{*params} is set to the corresponding \c{game_params} -structure. +Those fields describe the intended contents of one particular menu in +the hierarchy. \cq{entries} points to an array of \cq{n_entries} +items, each of which is a structure containing the following fields: + +\c struct preset_menu_entry { +\c char *title; +\c game_params *params; +\c struct preset_menu *submenu; +\c int id; +\c }; -Both of the two output values are dynamically allocated, but they -are owned by the mid-end structure: the front end should not ever -free them directly, because they will be freed automatically during -\cw{midend_free()}. +Of these fields, \cq{title} and \cq{id} are present in every entry, +giving (respectively) the textual name of the menu item and an integer +identifier for it. The integer id will correspond to the one returned +by \c{midend_which_preset} (\k{midend-which-preset}), when that preset +is the one selected. + +The other two fields are mutually exclusive. Each \c{struct +preset_menu_entry} will have one of those fields \cw{NULL} and the +other one non-null. If the menu item is an actual preset, then +\cq{params} will point to the set of game parameters that go with the +name; if it's a submenu, then \cq{submenu} instead will be non-null, +and will point at a subsidiary \c{struct preset_menu}. + +The complete hierarchy of these structures is owned by the mid-end, +and will be freed when the mid-end is freed. The front end should not +attempt to free any of it. + +The integer identifiers will be allocated densely from 0 upwards, so +that it's reasonable for the front end to allocate an array which uses +them as indices, if it needs to store information per preset menu +item. For this purpose, the front end may pass the second parameter +\cq{id_limit} to \cw{midend_get_presets} as the address of an \c{int} +variable, into which \cw{midend_get_presets} will write an integer one +larger than the largest id number actually used (i.e. the number of +elements the front end would need in the array). + +Submenu-type entries also have integer identifiers. \H{midend-which-preset} \cw{midend_which_preset()} @@ -3005,6 +3062,10 @@ no preset matches. Front ends could use this to maintain a tick beside one of the items in the menu (or tick the \q{Custom} option if the return value is less than zero). +The returned index value (if non-negative) will match the \c{id} field +of the corresponding \cw{struct preset_menu_entry} returned by +\c{midend_get_presets()} (\k{midend-get-presets}). + \H{midend-wants-statusbar} \cw{midend_wants_statusbar()} \c int midend_wants_statusbar(midend *me); @@ -3535,6 +3596,63 @@ single element (typically measured using \c{sizeof}). \c{rs} is a \c{random_state} used to generate all the random numbers for the shuffling process. +\H{utils-presets} Presets menu management + +The function \c{midend_get_presets()} (\k{midend-get-presets}) returns +a data structure describing a menu hierarchy. Back ends can also +choose to provide such a structure to the mid-end, if they want to +group their presets hierarchically. To make this easy, there are a few +utility functions to construct preset menu structures, and also one +intended for front-end use. + +\S{utils-preset-menu-new} \cw{preset_menu_new()} + +\c struct preset_menu *preset_menu_new(void); + +Allocates a new \c{struct preset_menu}, and initialises it to hold no +menu items. + +\S{utils-preset-menu-add_submenu} \cw{preset_menu_add_submenu()} + +\c struct preset_menu *preset_menu_add_submenu +\c (struct preset_menu *parent, char *title); + +Adds a new submenu to the end of an existing preset menu, and returns +a pointer to a newly allocated \c{struct preset_menu} describing the +submenu. + +The string parameter \cq{title} must be dynamically allocated by the +caller. The preset-menu structure will take ownership of it, so the +caller must not free it. + +\S{utils-preset-menu-add-preset} \cw{preset_menu_add_preset()} + +\c void preset_menu_add_preset +\c (struct preset_menu *menu, char *title, game_params *params); + +Adds a preset game configuration to the end of a preset menu. + +Both the string parameter \cq{title} and the game parameter structure +\cq{params} itself must be dynamically allocated by the caller. The +preset-menu structure will take ownership of it, so the caller must +not free it. + +\S{utils-preset-menu-lookup-by-id} \cw{preset_menu_lookup_by_id()} + +\c game_params *preset_menu_lookup_by_id +\c (struct preset_menu *menu, int id); + +Given a numeric index, searches recursively through a preset menu +hierarchy to find the corresponding menu entry, and returns a pointer +to its existing \c{game_params} structure. + +This function is intended for front end use (but front ends need not +use it if they prefer to do things another way). If a front end finds +it inconvenient to store anything more than a numeric index alongside +each menu item, then this function provides an easy way for the front +end to get back the actual game parameters corresponding to a menu +item that the user has selected. + \H{utils-alloc} Memory allocation Puzzles has some central wrappers on the standard memory allocation diff --git a/dominosa.c b/dominosa.c index dc7c2c7..c86ba19 100644 --- a/dominosa.c +++ b/dominosa.c @@ -1709,7 +1709,7 @@ static void game_print(drawing *dr, const game_state *state, int tilesize) const struct game thegame = { "Dominosa", "games.dominosa", "dominosa", default_params, - game_fetch_preset, + game_fetch_preset, NULL, decode_params, encode_params, free_params, diff --git a/emcc.c b/emcc.c index 74f17cf..ca033cb 100644 --- a/emcc.c +++ b/emcc.c @@ -61,7 +61,8 @@ extern void js_debug(const char *); extern void js_error_box(const char *message); extern void js_remove_type_dropdown(void); extern void js_remove_solve_button(void); -extern void js_add_preset(const char *name); +extern void js_add_preset(int menuid, const char *name, int value); +extern int js_add_preset_submenu(int menuid, const char *name); extern int js_get_selected_preset(void); extern void js_select_preset(int n); extern void js_get_date_64(unsigned *p); @@ -552,6 +553,21 @@ static game_params **presets; static int npresets; int have_presets_dropdown; +void populate_js_preset_menu(int menuid, struct preset_menu *menu) +{ + int i; + for (i = 0; i < menu->n_entries; i++) { + struct preset_menu_entry *entry = &menu->entries[i]; + if (entry->params) { + presets[entry->id] = entry->params; + js_add_preset(menuid, entry->title, entry->id); + } else { + int js_submenu = js_add_preset_submenu(menuid, entry->title); + populate_js_preset_menu(js_submenu, entry->submenu); + } + } +} + void select_appropriate_preset(void) { if (have_presets_dropdown) { @@ -787,23 +803,16 @@ int main(int argc, char **argv) * Set up the game-type dropdown with presets and/or the Custom * option. */ - npresets = midend_num_presets(me); - if (npresets == 0) { - /* - * This puzzle doesn't have selectable game types at all. - * Completely remove the drop-down list from the page. - */ - js_remove_type_dropdown(); - have_presets_dropdown = FALSE; - } else { + { + struct preset_menu *menu = midend_get_presets(me, &npresets); presets = snewn(npresets, game_params *); - for (i = 0; i < npresets; i++) { - char *name; - midend_fetch_preset(me, i, &name, &presets[i]); - js_add_preset(name); - } + for (i = 0; i < npresets; i++) + presets[i] = NULL; + + populate_js_preset_menu(0, menu); + if (thegame.can_configure) - js_add_preset(NULL); /* the 'Custom' entry in the dropdown */ + js_add_preset(0, "Custom", -1); have_presets_dropdown = TRUE; diff --git a/emcclib.js b/emcclib.js index 1dde2b3..cd8876e 100644 --- a/emcclib.js +++ b/emcclib.js @@ -59,28 +59,17 @@ mergeInto(LibraryManager.library, { }, /* - * void js_add_preset(const char *name); + * void js_add_preset(int menuid, const char *name, int value); * - * Add a preset to the drop-down types menu. The provided text is - * the name of the preset. (The corresponding game_params stays on - * the C side and never comes out this far; we just pass a numeric - * index back to the C code when a selection is made.) - * - * The special 'Custom' preset is requested by passing NULL to - * this function. - */ - js_add_preset: function(ptr) { - var name = (ptr == 0 ? "Custom" : Pointer_stringify(ptr)); - var value = gametypeitems.length; - + * Add a preset to the drop-down types menu, or to a submenu of + * it. 'menuid' specifies an index into our array of submenus + * where the item might be placed; 'value' specifies the number + * that js_get_selected_preset() will return when this item is + * clicked. + */ + js_add_preset: function(menuid, ptr, value) { + var name = Pointer_stringify(ptr); var item = document.createElement("li"); - if (ptr == 0) { - // The option we've just created is the one for inventing - // a new custom setup. - gametypecustom = item; - value = -1; - } - item.setAttribute("data-index", value); var tick = document.createElement("span"); tick.appendChild(document.createTextNode("\u2713")); @@ -88,7 +77,7 @@ mergeInto(LibraryManager.library, { tick.style.paddingRight = "0.5em"; item.appendChild(tick); item.appendChild(document.createTextNode(name)); - gametypelist.appendChild(item); + gametypesubmenus[menuid].appendChild(item); gametypeitems.push(item); item.onclick = function(event) { @@ -99,6 +88,34 @@ mergeInto(LibraryManager.library, { } }, + /* + * int js_add_preset_submenu(int menuid, const char *name); + * + * Add a submenu in the presets menu hierarchy. Returns its index, + * for passing as the 'menuid' argument in further calls to + * js_add_preset or this function. + */ + js_add_preset_submenu: function(menuid, ptr, value) { + var name = Pointer_stringify(ptr); + var item = document.createElement("li"); + // We still create a transparent tick element, even though it + // won't ever be selected, to make submenu titles line up + // nicely with their neighbours. + var tick = document.createElement("span"); + tick.appendChild(document.createTextNode("\u2713")); + tick.style.color = "transparent"; + tick.style.paddingRight = "0.5em"; + item.appendChild(tick); + item.appendChild(document.createTextNode(name)); + var submenu = document.createElement("ul"); + submenu.className = "left"; + item.appendChild(submenu); + gametypesubmenus[menuid].appendChild(item); + var toret = gametypesubmenus.length; + gametypesubmenus.push(submenu); + return toret; + }, + /* * int js_get_selected_preset(void); * diff --git a/emccpre.js b/emccpre.js index b10bf29..d715858 100644 --- a/emccpre.js +++ b/emccpre.js @@ -82,8 +82,9 @@ var dlg_return_sval, dlg_return_ival; // The