Generic nested menus implementation
- To: mathgroup at smc.vnet.net
- Subject: [mg100649] Generic nested menus implementation
- From: Leonid Shifrin <lshifr at gmail.com>
- Date: Wed, 10 Jun 2009 05:34:33 -0400 (EDT)
Hi all, since already two people asked for this feature, and I got interested myself, I assume that this topic may be of general interest, and this is my reason to start a separate thread. Here I present my first attempt at generic multilevel menu implementation. The code is probably full of bugs, and also perhaps many parts could be done easier but I just did not figure it out, so I will appreciate any feedback. The examples of use follow. THE CODE ----------------------------------------------------------------------------------------------------------------------- (* checking recursive pattern *) Clear[menuTreeValidQ]; menuTreeValidQ[{_String, {___String}}] := True; menuTreeValidQ[item_] := MatchQ[item, {_String, {___?menuTreeValidQ} ..}]; (* This can probably be done much better *) Clear[localMaxPositions]; localMaxPositions[ls_List] := Module[{n = 0, pos, part}, pos = Position[part = Split[Partition[ls, 3, 1], Last[#1] == First[#2] &], x_ /; MatchQ[x, {{s_, t_, u_}} /; s <= t && u <= t] || MatchQ[x, {{s_, t_, _}, ___, {_, u_, p_}} /; s <= t && u >= p]]; List /@ Flatten[Fold[ Function[{plist, num}, n++; Join[plist[[1 ;; n - 1]], Range[plist[[n]], plist[[n]] + num - 1], plist[[n + 1 ;;]] + num - 1]], pos, Length /@ Extract[part, pos]] + 1]]; (* By leaves I here mean, for every branch, those with locally largest distance from the stem. Level would not do *) Clear[leafPositions]; leafPositions[tree_] := Extract[#, localMaxPositions[Length /@ #]] &@ Reap[MapIndexed[Sow[#2] &, tree , Infinity]][[2, 1]]; Clear[mapOnLeaves]; mapOnLeaves[f_, tree_] := MapAt[f, tree, leafPositions[tree]]; Clear[replaceByEvaluated]; replaceByEvaluated[expr_, patt_] := With[{pos = Position[expr, patt]}, With[{newpos = Split[Sort[pos, Length[#1] > Length[#2] &], Length[#1] == Length[#2] &]}, Fold[ReplacePart[#1, Extract[#1, #2], #2, List /@ Range[Length[#2]]] &, expr, newpos]]]; (* converts initial string-based menu tree into more complex expression suitable for menu construction *) Clear[menuItemsConvertAlt]; menuItemsConvertAlt[menuitemTree_, menuMakers : {(_Symbol | _Function) ..}, actionF_, representF_: (# &), representLeavesF_: (# &)] := Module[{g, actionAdded, interm, maxdepth, actF}, actionAdded = mapOnLeaves[If[# === {}, Sequence @@ #, representLeavesF[#] :> actF[#]] &, menuitemTree]; interm = MapIndexed[ Replace[#, {x_String, y : ({({_RuleDelayed} | _RuleDelayed) ..})} :> representF[x, {Length[#2]/2}] :> g[Length[#2]/2][x, Flatten@y], 1] &, {actionAdded}, Infinity]; interm = Fold[replaceByEvaluated, interm, {_Replace, HoldPattern[# &[__]], HoldPattern[Length[{___}]], HoldPattern[Times[_Integer, Power[_Integer, -1]]], _Flatten}][[1, 2]]; maxdepth = Max[Cases[interm, g[x_] :> x, Infinity, Heads -> True]]; With[{fns = menuMakers}, replaceByEvaluated[interm /. g[n_Integer] :> menuMakers[[n]], HoldPattern[fns[[_Integer]]]] /. actF :> actionF ] /; maxdepth <= Length[menuMakers]] (* main menu-building function *) Clear[createNestedMenu]; createNestedMenu::invld = "The supplied menu tree structure is not valid"; createNestedMenu[menuItemTree_, ___] /; Not[menuTreeValidQ[menuItemTree]] := "never happens" /; Message[createNestedMenu::invld]; createNestedMenu[menuItemTree_?menuTreeValidQ, menuCategories_, actionF_, representF_: (# &), representLeavesF_: (# &)] := Module[ {menuVars, menuNames, menuDepth = Depth[Last@menuItemTree]/2, setHeld, setDelayedHeld, heldPart, subBack, subBackAll, makeSubmenu, addSpaces, menuCategs = menuCategories, standardCategory = " Choose "}, Options[makeSubmenu] = {Appearance -> "Button", FieldSize -> {{1, 8}, {1, 4}}, Background -> Lighter[Yellow, 0.8], BaseStyle -> {FontFamily -> "Helvetica", FontColor -> Brown, FontWeight -> Plain}}; menuCategs = PadRight[menuCategs, menuDepth + 1, standardCategory]; (* Make variables to store menu names and values *) Block[{var, name}, {menuVars, menuNames} = Apply[ Hold, Evaluate[Table[Unique[#], {menuDepth}]]] & /@ {var, name}]; (* Functions to set/extract held variables *) setHeld[Hold[var_], rhs_] := var = rhs; setDelayedHeld[Hold[var_], rhs_] := var := rhs; heldPart[seq_Hold, n_] := First[Extract[seq, {{n}}, Hold]]; (* Functions to close the given menu/submenus*) subBack[depth_Integer] := (If[depth =!= menuDepth + 1, setHeld[heldPart[menuVars, depth], ""]]; setHeld[heldPart[menuNames, depth - 1], menuCategs[[depth - 1]]]); subBackAll[depth_Integer] := subBack /@ Range[menuDepth + 1, depth, -1]; (* Function to create a (sub)menu at a given level *) makeSubmenu[depth_] := Function[{nm, actions}, subBackAll[depth + 1];(* remove lower menus if they are open *) If[depth =!= 1, setHeld[heldPart[menuNames, depth - 1], nm]]; setDelayedHeld[heldPart[menuVars, depth], Dynamic@ ActionMenu[menuNames[[depth]], If[depth === 1, actions, Prepend[actions, "Back" :> subBackAll[depth]]], AutoAction -> True, Sequence @@ Options[makeSubmenu]]]]; (* Function to help with a layout *) addSpaces[x_List, spaceLength : (_Integer?Positive) : 10] := With[{space = StringJoin @@ Table[" ", {spaceLength}]}, MapIndexed[ReplacePart[Table[space, {Length[x]}], ##] &, x]]; (* Initialization code *) subBackAll[2]; menuItemsConvertAlt[ {menuNames[[1]], {menuItemTree}}, makeSubmenu /@ Range[menuDepth], actionF, representF, representLeavesF][[1, 2]]; (* Display the menus *) Dynamic[ Function[Null, Grid[addSpaces[{##}, 5], Frame -> True, FrameStyle -> Thick, Background -> Lighter[Pink, 0.8]], HoldAll] @@ menuVars] ]; (* End Module *) ----------------------------------------------------------------------------------------------------------- EXAMPLES and explanation So, the input for a menu should be a tree structure like this: In[1] = menuItems = {"Continents", {{"Africa", {{"Algeria", {"Algiers", "Oran"}}, {"Angola", {"Luanda", "Huambo"}}}}, {"North America", {{"United States", {"New \ York", "Washington"}}, {"Canada", {"Toronto", "Montreal"}}}}}}; The root of the tree ("Continents" in this case) is not used later (but needed for consistency), so can be any string. The second necessary ingredient is a list of categories (strings) of the length equal to the depth of the menu to be constructed, or less (in which case some subcategories will be shown with a standard header "Choose"). The last mandatory ingredient is a function representing the action to be taken upon clicking on the lowest-level menu item (leaf). This is how we create the menu: In[1] = createNestedMenu[menuItems, {"Continent", " Country ", " City "}, Print] In this case, all categories are given explicilty, and when we click on an "atomic" menu element (not representing further menu sub-levels), it is printed. You can also omit some sub-categories, they will be substituted by "Choose" In[2] = createNestedMenu[menuItems, {"Continent"}, Print] There are additional optional parameters, which allow us to represent different submenu items in different way - functions representF and representLeavesF. The first one governs the appearance of the non-atomic submenu elements and takes the level of a submenu as a second argument. The second governs the appearance of atomic menu elements (leaves). For example: In[3] = createNestedMenu[menuItems, {"Continent", " Country ", " City "}, Print, (Style[#, FontColor -> Switch[#2, {1}, Brown, {2}, Blue, {3}, Green, _True, Orange], #]) &, Style[#, Red] &] I went through some pains to ensure that the menu will work also on less regular menu trees, where leaves may have different distance from the stem, like here: In[4] = menuTreeValidQ@ (compMenuItems = {"Company", {{"Services", {"Training", "Development"}}, {"Products", {{"OS tools", {}}, {"Application software", {}} }}, {"News", {{"Press releases", {}}, {"Media coverage", {}} }}, {"Company", {{"Vacancies", {{"Developer", {"Requirements"}}, {"Tester", {}}}}, {"Structure", {}}} } }}) Out[4] = True We create the menu as before: In[5] = createNestedMenu[compMenuItems, {"Main"}, Print] where I omitted sub-categories. Notice that the syntax I chose is such that, whenever the submenu contains only atomic elements, they can either all be represented by just strings, or wrapped in lists as {element,{}}, but not mixed. But if menu contains a mix of atomic and non-atomic elements, atomic elements must be wrapped in lists as above. For example, the more "politically correct" way to represent the first example structrure is this: {"Continents", {{"Africa", {{"Algeria", {{"Algiers", {}}, {"Oran", \ {}}}}, {"Angola", {{"Luanda", {}}, {"Huambo", {}}}}}}, {"North \ America", {{"United States", {{"New York", {}}, {"Washington", {}}}}, \ {"Canada", {{"Toronto", {}}, {"Montreal", {}}}}}}}} The menuTreeValidQ predicate can be used to test if the structure is valid or not. Hope that I don't waste everyone's time and bandwidth. All feedback is greatly appreciated. Regards, Leonid
- Follow-Ups:
- Re: Generic nested menus implementation
- From: Leonid Shifrin <lshifr@gmail.com>
- Re: Generic nested menus implementation