Using menu's

A lot of software makes use of menu's. Menu's are a means for the user to perform certain actions with the application. Often even the simplest applications have a menu built in, beit small, beit extensive and all variations in between. Menu's are specified mostly by using struct NewMenu as found in libraries/gadtools.h and may look like this ('reverse engineered' from NotePad...:
  1. struct NewMenu NM[] = {{NM_TITLE, "Project", NULL, 0, 0, NULL}
  2. ,{ NM_ITEM, "New", "N", 0, 0, NULL}
  3. ,{ NM_ITEM, "Open...", "O", 0, 0, NULL}
  4. ,{ NM_ITEM, "Insert...", NULL, 0, 0, NULL}
  5. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  6. ,{ NM_ITEM, "Save", "S", 0, 0, NULL}
  7. ,{ NM_ITEM, "Save as...", NULL, 0, 0, NULL}
  8. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  9. ,{ NM_ITEM, "Print...", "P", 0, 0, NULL}
  10. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  11. ,{ NM_ITEM, "Close text", NULL, 0, 0, NULL}
  12. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  13. ,{ NM_ITEM, "About...", NULL, 0, 0, NULL}
  14. ,{ NM_ITEM, "Iconify", NULL, 0, 0, NULL}
  15. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  16. ,{ NM_ITEM, "Quit", "Q", 0, 0, NULL}
  17. ,{NM_TITLE, "Update", NULL, 0, 0, NULL}
  18. ,{ NM_ITEM, "Cut", "X", 0, 0, NULL}
  19. ,{ NM_ITEM, "Copy", "C", 0, 0, NULL}
  20. ,{ NM_ITEM, "Paste", "V", 0, 0, NULL}
  21. ,{ NM_ITEM, "Mark All", "M", 0, 0, NULL}
  22. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  23. ,{ NM_ITEM, "Clear", NULL, 0, 0, NULL}
  24. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  25. ,{ NM_ITEM, "Undo", "U", 0, 0, NULL}
  26. ,{ NM_ITEM, "Redo", "Z", 0, 0, NULL}
  27. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  28. ,{ NM_ITEM, "Clear All", NULL, 0, 0, NULL}
  29. ,{NM_TITLE, "Navigate", NULL, 0, 0, NULL}
  30. ,{ NM_ITEM, "Find...", "F", 0, 0, NULL}
  31. ,{ NM_ITEM, "Find next", ".", 0, 0, NULL}
  32. ,{ NM_ITEM, "Find & replace", "R", 0, 0, NULL}
  33. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  34. ,{ NM_ITEM, "Jump to...", "J", 0, 0, NULL}
  35. ,{NM_TITLE, "Settings", NULL, 0, 0, NULL}
  36. ,{ NM_ITEM, "Make icons", NULL, 0, 0, NULL}
  37. ,{ NM_ITEM, "Auto indentation", NULL, 0, 0, NULL}
  38. ,{ NM_ITEM, "Select font...", NULL, 0, 0, NULL}
  39. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  40. ,{ NM_ITEM, "Save settings", NULL, 0, 0, NULL}
  41. ,{ NM_END, NULL NULL, 0, 0, NULL}
  42. };
So far nothing spectacular. That facet will come when we get a IDCMP message telling us a menu item has been choosen by the user and subsequently we want to decypher which one that was and act upon that. In the message loop code like this may be encountered, which is only partially provided here as all-in-all it is quite extensive! Even for a relatively small menu strip. The more extended a menustrip is, the more tedious it becomes to decypher and properly handle the user's choice. The code provided does not even provide for SubItem handling, which would make it even more clunky as in every applicable case of MENUITEM(WMHI_Message & WMHI_MENUMASK), a subsequent switch (MENUSUB(WMHI_Message & WMHI_MENUMASK)), followed with all its relevant cases has to be written out. What's more; the moment you are going to add an item (or subitem), or remove it or put it in a different place, you have to reorganise the entire WMHI_MENUPICK-case to reflect those changes, every time you make any changes. A really cumbersome approach, as eachtime the structure of Menu, Item and SubItem has changed. Do you really want that?
  1. BOOL Done = FALSE;
  2. uint32 AnySig;
  3.  
  4. while (!Done)
  5. {
  6. AnySig = IExec->Wait(AllSigs);
  7.  
  8. if (AnySig & OurWindowSig)
  9. {
  10. while ((WMHI_Message = IIntuition->IDoMethod(WinObj, WM_HANDLEINPUT, 0)) != WMHI_LASTMSG)
  11. {
  12. switch (WMHI_Message & WMHI_CLASSMASK)
  13. {
  14. case WMHI_MENUPICK:
  15. {
  16. switch (MENUNUM(WMHI_Message & WMHI_MENUMASK))
  17. {
  18. case 0: // Project
  19. {
  20. switch (MENUITEM(WMHI_Message & WMHI_MENUMASK))
  21. {
  22. case 0: // Project --> New
  23. {
  24.  
  25. break;
  26. }
  27.  
  28. case 1: // Project --> Open
  29. {
  30.  
  31. break;
  32. }
  33.  
  34. case 2: // Project --> Insert
  35. {
  36.  
  37. break;
  38. }
  39.  
  40. // case 3: does not exist as that position is held by a nonselectable barlabel.
  41.  
  42. case 4: // Project --> Save
  43. {
  44.  
  45. break;
  46. }
  47.  
  48. case 5: // Project --> SaveAs
  49. {
  50.  
  51. break;
  52. }
  53.  
  54. // case 6: does not exist as that position is held by a nonselectable barlabel.
  55.  
  56. case 7: // Project --> Print
  57. {
  58.  
  59. break;
  60. }
  61.  
  62. // case 8: does not exist as that position is held by a nonselectable barlabel.
  63.  
  64. case 9: // Project --> Close text
  65. {
  66.  
  67. break;
  68. }
  69.  
  70. // case 10: does not exist as that position is held by a nonselectable barlabel.
  71.  
  72. case 11: // Project --> About
  73. {
  74.  
  75. break;
  76. }
  77.  
  78. case 12: // Project --> Iconify
  79. {
  80.  
  81. break;
  82. }
  83.  
  84. // case 13: does not exist as that position is held by a nonselectable barlabel.
  85.  
  86. case 14: // Project --> Quit
  87. {
  88.  
  89. break;
  90. }
  91. }
  92.  
  93. break;
  94. }
  95.  
  96. case 1: // Update
  97. {
  98.  
  99. break;
  100. }
  101.  
  102. case 2: // Navigate
  103. {
  104.  
  105. break;
  106. }
  107.  
  108. case 3: // Settings
  109. {
  110.  
  111. break;
  112. }
  113. }
  114.  
  115. break;
  116. }
  117. }
  118. }
  119. }
  120. }
In this snippet I've only elaborated to some extent on the 'Project'-menu. It is hardly imaginable that anybody really likes such a setup and wants to write the code for it. The C++-style comments at every relevant case are there to keep you as a maintainer sane and are a clear sign of the awkward nature of this method. That is exactly why I, for one, have a strong dislike for it and have come up with a solution, which makes use of a curiosity. That curiosity bears down to the fact that addresses of allocations and functions always start at a four-byte boundery and so leave the last 2 bits, the least significant ones, always 0. We can put that 'wasted' space to good use, as 2 bits give us hold of 4 different values. We actually only need 2 in this case, but the code provided here is set up for 4. 'struct NewMenu' has a member called 'nm_UserDate' and is of type 'APTR' so all values have to be cast to APTR. That very member we are going to use for our experiment. We will use it for either an ordinal number, defined by an enumeration, or for an address of a function. And those 2 least significant bits will later on tell us what we have... Allright lets redo our code. First of all, we need that enumeration. Not for all entries, but only for those entries which won't make use of a function of their own. Like 'Project/Quit': the only thing it will do is setting the BOOLean 'Done' to TRUE. I'll only set this up for menu 'Project' in order to keep things edible. Options 'Open...', 'Insert...', 'Close text' and 'Quit' will for no specific reason be handled in the traditional way, meaning use of a switch-and-case sequence, the others by calling their provided function. The switch-and-case sequence however is now no longer driven by Menu, Item and SubItem positions as in the original way, but by an ordinal number, enumerated value. You end up with only one switch-and-case sequence for the entire menusystem in stead of the nested current approach of a switch-and-case sequence for every Menu and within each menu for each Item and eventually each SubItem. The enumeration:
  1. enum {MOID_OPEN
  2. ,MOID_QUIT
  3. ,MOID_CLOSETEXT
  4. ,MOID_INSERT
  5. ,MOID_LAST
  6. };
The functions:
  1. BOOL NewProject(<A uniform set of parameters>)
  2. {
  3. BOOL Success = FALSE;
  4.  
  5. /*
  6.   ** Do here something, and when the results are OK
  7.   ** set Success to TRUE.
  8.   */
  9.  
  10. return Success;
  11. }
  12.  
  13. BOOL SaveProject(<A uniform set of parameters>)
  14. {
  15. BOOL Success = FALSE;
  16.  
  17. /*
  18.   ** Do here something, and when the results are OK
  19.   ** set Success to TRUE.
  20.   */
  21.  
  22. return Success;
  23. }
  24.  
  25. BOOL SaveProjectAs(<A uniform set of parameters>)
  26. {
  27. BOOL Success = FALSE;
  28.  
  29. /*
  30.   ** Do here something, and when the results are OK
  31.   ** set Success to TRUE.
  32.   */
  33.  
  34. return Success;
  35. }
  36.  
  37. BOOL PrintProject(<A uniform set of parameters>)
  38. {
  39. BOOL Success = FALSE;
  40.  
  41. /*
  42.   ** Do here something, and when the results are OK
  43.   ** set Success to TRUE.
  44.   */
  45.  
  46. return Success;
  47. }
  48.  
  49. BOOL About(<A uniform set of parameters>)
  50. {
  51. BOOL Success = FALSE;
  52.  
  53. /*
  54.   ** Do here something, and when the results are OK
  55.   ** set Success to TRUE.
  56.   */
  57.  
  58. return Success;
  59. }
  60.  
  61. BOOL IconifyProject(<A uniform set of parameters>)
  62. {
  63. BOOL Success = FALSE;
  64.  
  65. /*
  66.   ** Do here something, and when the results are OK
  67.   ** set Success to TRUE.
  68.   */
  69.  
  70. return Success;
  71. }
Now the menu is to be set up again, but with a little difference to the previous one, as the last entry, which reflects nm_UserData, on every applicable line is now receiving a value:
  1. struct NewMenu NM[] = {{NM_TITLE, "Project", NULL, 0, 0, NULL}
  2. ,{ NM_ITEM, "New", "N", 0, 0, (APTR)NewProject}
  3. ,{ NM_ITEM, "Open...", "O", 0, 0, (APTR)((MOID_OPEN << 2) | 1)}
  4. ,{ NM_ITEM, "Insert...", NULL, 0, 0, (APTR)((MOID_INSERT << 2) | 1)}
  5. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  6. ,{ NM_ITEM, "Save", "S", 0, 0, (APTR)SaveProject}
  7. ,{ NM_ITEM, "Save as...", NULL, 0, 0, (APTR)SaveProjectAs}
  8. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  9. ,{ NM_ITEM, "Print...", "P", 0, 0, (APTR)PrintProject}
  10. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  11. ,{ NM_ITEM, "Close text", NULL, 0, 0, (APTR)((MOID_CLOSETEXT << 2) | 1)}
  12. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  13. ,{ NM_ITEM, "About...", NULL, 0, 0, (APTR)About}
  14. ,{ NM_ITEM, "Iconify", NULL, 0, 0, (APTR)IconifyProject}
  15. ,{ NM_BARLABEL, NULL, NULL, 0, 0, NULL}
  16. ,{ NM_ITEM, "Quit", "Q", 0, 0, (APTR)((MOID_QUIT << 2) | 1)}
  17.  
  18.  
  19. ,{ NM_END, NULL NULL, 0, 0, NULL}
  20. };
Clarification of this code with respect to nm_Userdata: '(APTR)NewProject' is the address of function 'NewProject' cast to an APTR, while '((MOID_OPEN << 2) | 1)' is the enumerated value of MOID_OPEN, which in turn is shifted to the left by 2 positions, after which it is 'or'-ed with '1' to make it known that this is an enumerated value and the whole value is then cast to APTR. And our code for the WMHI_MENUPICK-event may, again partially, look like this:
  1. BOOL Done = FALSE;
  2. BOOL Success;
  3. uint32 AnySig,
  4. WMHI_Message;
  5. uint16 MenuIndex;
  6. uint32 ActionType,
  7. ActionValue;
  8.  
  9. while (!Done)
  10. {
  11. AnySig = IExec->Wait(AllSigs);
  12.  
  13. if (AnySig & OurWindowSig)
  14. {
  15. while ((WMHI_Message = IIntuition->IDoMethod(WinObj, WM_HANDLEINPUT, 0)) != WMHI_LASTMSG)
  16. {
  17. switch (WMHI_Message & WMHI_CLASSMASK)
  18. {
  19. case WMHI_MENUPICK:
  20. {
  21. /*
  22.   ** First thing we want to know which item is selected by the user.
  23.   ** The function Retrieve_MenuIndex provides us with that:
  24.   */
  25. MenuIndex = Retrieve_MenuIndex((WMHI_Message & ~WMHI_CLASSMASK), NM);
  26.  
  27. ActionType = ((uint32)NM[MenuIndex].nm_UserData) & 3);
  28. ActionValue = ((uint32)NM[MenuIndex].nm_UserData) & (uint32)~3);
  29.  
  30. switch (ActionType)
  31. {
  32. case 0: // Will only be used for 'pure' addresses, so functions which we will call:
  33. {
  34. /*
  35.   ** Be sure to call an address and not a NULL-value,
  36.   ** as doing so will lead to a crash, something you
  37.   ** are probably not after:
  38.   **
  39.   ** N.b.: This is the ONLY point from which the functions are called, when asked
  40.   ** for by the menu, saving you from lots and lots and lots of tedious code!
  41.   */
  42. if (ActionValue & (uint32)~3)
  43. {
  44. Success = ((BOOL (*)())ActionValue(<Here goes the uniform set of parameters!>);
  45. }
  46.  
  47. break;
  48. }
  49.  
  50. /*
  51.   ** When ActionType is '1', We will use the
  52.   ** enumeration values to determine what to do:
  53.   */
  54. case 1:
  55. {
  56. switch (ActionValue)
  57. {
  58. case MOID_QUIT:
  59. {
  60. Done = TRUE;
  61.  
  62. break;
  63. }
  64.  
  65. case MOID_OPEN:
  66. {
  67. /*
  68.   ** Provide code here that Opens a file etc., etc.
  69.   */
  70.  
  71. break;
  72. }
  73.  
  74. case MOID_CLOSETEXT:
  75. {
  76. /*
  77.   ** Provide here some code that will do
  78.   ** what the user expects it to do:
  79.   */
  80.  
  81. break;
  82. }
  83.  
  84. case MOID_INSERT:
  85. {
  86. /*
  87.   ** Perform the insertion here.
  88.   ** You may even call a function here...
  89.   */
  90.  
  91. break;
  92. }
  93. }
  94.  
  95. break;
  96. }
  97.  
  98. break;
  99. }
  100. }
  101. }
  102. }
  103. }
When Menu's are added, removed or changed, little needs to be done: in case an enumerated option is added or removed then the switch-and-case statement needs to be adjusted and maybe the enumeration. Rearranging the entire menu, even on a grand scale, does not require any changes in the WMHI_MENUPICK-portion of the code. However, if an option is added to (or removed from) the menu with a functionpointer in it nm_UserData-field, then only that function needs to be added (or removed). In that case the WMHI_MENUPICK-portion of the code requires no changes at all! And finally the function that provides us with the ordinal number of the user's choice:
  1. uint32 Retrieve_MenuChoice(uint32 MenuChoice, struct NewMenu *MenuList)
  2. {
  3. uint32 Index = ~0; // Set to an error value of 0xFFFFFFFF
  4.  
  5. /*
  6.   ** Has a choice been made?
  7.   ** 31 if not.
  8.   */
  9. if (MENUNUM(MenuChoice) != 31)
  10. {
  11. Index = 0;
  12.  
  13. while ((MenuList[notag][Index][/notag].nm_Type != NM_END) &&
  14. (MENUNUM(MenuChoice) > 0)
  15. )
  16. {
  17. if (MenuList[notag][Index][/notag].nm_Type == NM_TITLE)
  18. {
  19. /*
  20.   ** Menu's index take up the 5 least significant bits:
  21.   */
  22. MenuChoice -= 1;
  23. Index++;
  24.  
  25. while (MenuList[notag][Index][/notag].nm_Type != NM_TITLE)
  26. {
  27. Index++;
  28. }
  29. }
  30. }
  31.  
  32. Index++;
  33.  
  34. while ((MenuList[notag][Index][/notag].nm_Type != NM_END) &&
  35. (ITEMNUM(MenuChoice) > 0)
  36. )
  37. {
  38. if (MenuList[notag][Index][/notag].nm_Type == NM_ITEM)
  39. {
  40. /*
  41.   ** MenuItem's index take up the middle 6 bits:
  42.   */
  43. MenuChoice -= (1 << 5);
  44. Index++;
  45.  
  46. while (MenuList[notag][Index][/notag].nm_Type != NM_ITEM)
  47. {
  48. Index++;
  49. }
  50. }
  51. }
  52.  
  53. if (SUBNUM(MenuChoice) != 31)
  54. {
  55. Index++;
  56.  
  57. if (SUBNUM(MenuChoice) > 0)
  58. {
  59. while ((MenuList[notag][Index][/notag].nm_Type != NM_END) &&
  60. (SUBNUM(MenuChoice) > 0)
  61. )
  62. {
  63. if (MenuList[notag][Index][/notag].nm_Type == NM_SUB)
  64. {
  65. /*
  66.   ** Menu's index take up the 5 most significant bits:
  67.   */
  68. MenuChoice -= (1 << 11);
  69. }
  70.  
  71. Index++;
  72. }
  73. }
  74. }
  75. }
  76.  
  77. return(Index);
  78. }
This may not neccessarily give you faster executable code, maybe a smaller executable, but that is not the primary aim. The profit must be sought in the size of the sourcecode for WMHI_MENUPICK-event, its readabillity and, hence, its maintainabillity. Actually, come to think of it, the whole menusystem could use an overhaul and be 'Objectified' in a ReAction-befitting manner.

Tags: 

Blog post type: 

Comments

whose's picture
Good one, again ;)
Coder Insane-Software www.insane-software.de
YesCop's picture
Interesting. I am not expert in guis but in Retrieve_MenuChoice function, the menulist variable doesn't exist, couldn't be NM ? And may be the line 17 contains an error ? MenuList#'['Index#'] If not, it is a new C grammar... I take a quick look at the Retrieve function, there is one think that i don't understand. It is MenuChoice -= (1 << 11) or MenuChoice -= (1 << 5); Could you explain why you used 11 and 5 ? Thanks.
OldFart's picture
@YesCop >I am not expert in guis but in Retrieve_MenuChoice function, the menulist variable doesn't exist, couldn't be NM ? Yes, you are right. A last minute change caused this error. >And may be the line 17 contains an error ? MenuList#'['Index#'] If not, it is a new C grammar... It's not new C grammar ;-). I found it hard on this site to have a square bracket in the text as anything between a pair a square brackets is regarded a tag. What you encountered is the result of one of many failed attempts to get it right. Right after that I discoverd the use of [notag][notag][/notag] and [notag][/notag][/notag] pair of tags. It should be: NM[notag][Index].nm_Type[/notag]. (Ihope things come through as I intended.) >I take a quick look at the Retrieve function, there is one think that i don't understand. It is MenuChoice -= (1 << 11) or MenuChoice -= (1 << 5); Could you explain why you used 11 and 5 ? Intuition uses 16bits to store a menu choice: the upper 5 bits are used for an menuSUBitem, the next 6 bits re used for a menuITEM and the final 5, the least signficant ones, are used for the MENU. To retrieve a proper number for a menu item, the 16-bit value has to be shifted to the right. To even get a proper value for a subitem, said value has to be shifted to the right over 6 + 5 = 11 positions. Maybe things are clarified a bit more when you look up ITEMNUM(n), SUBNUM(n) and MENUNUM(n) in intuition/intuition.h. >Thanks. You're welcome ;-)
YesCop's picture
OldFart, Your explanations are clear, I have understood all what you wrote. :) Thanks again.