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: -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()}. +\c struct preset_menu_entry { +\c char *title; +\c game_params *params; +\c struct preset_menu *submenu; +\c int id; +\c }; + +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. + * 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(ptr) { - var name = (ptr == 0 ? "Custom" : Pointer_stringify(ptr)); - var value = gametypeitems.length; - + 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