Copyright (c) Hyperion Entertainment and contributors.

Difference between revisions of "Intuition Menu Class"

From AmigaOS Documentation Wiki
Jump to navigation Jump to search
Line 219: Line 219:
 
Additionally, as of Intuition V54 it is no longer mandatory to remove the menus with ClearMenuStrip() before closing the window. CloseWindow() itself will take care of clearing the menus if needed (no matter if BOOPSI or old-style).
 
Additionally, as of Intuition V54 it is no longer mandatory to remove the menus with ClearMenuStrip() before closing the window. CloseWindow() itself will take care of clearing the menus if needed (no matter if BOOPSI or old-style).
   
Note: it is still required, however, to use ClearMenuStrip() to remove the menu strip of a window before setting a different one with SetMenuStrip().
+
{{Note|text=It is still required, however, to use ClearMenuStrip() to remove the menu strip of a window before setting a different one with SetMenuStrip().}}
   
 
== Handling menu item selections by the user ==
 
== Handling menu item selections by the user ==

Revision as of 05:32, 9 July 2015

THE NEW BOOPSI MENU CLASS

An overview

Starting with version 54.6, Intuition features a built-in BOOPSI menu class ("menuclass") which aims to fully replace the traditional way of setting up and handling menus in applications.

Its object-oriented API allows to easily build complex menu trees, freeing a program from the need to use gadtools.library for that job. It also provides extensive control over menus, making it possible to define, read and change their properties thanks to simple yet flexible methods and attributes.

Moreover, menuclass extends the capabilities of the original Intuition menu system. For instance, it can handle any number of sub-menu levels, as well as any amount of items in menus and sub-menus. For nearly all operations, it transparently synchronizes itself with Intuition and performs menu relayout if needed. Automatic placement of custom images in front of menu item labels is also supported.

BOOPSI menus vs. traditional menus

BOOPSI menus may look and feel identical to traditional ones from the point of view of the user, but are handled quite differently in application code.

The old-style, white-box Menu and MenuItem structures are replaced by abstract objects (instances of menuclass) accessed through an object-oriented interface, removing the need for manual poking or dedicated menu functions.

Such BOOPSI menu objects are able to perform automatically most operations for which their traditional counterparts would require special assistance from the application.

Types of menuclass objects

There exist three distinct types of menuclass objects -- menu root, menu and menu item. Any instance of the class belongs to one of these types, specified at OM_NEW time through the MA_Type attribute.

The possible values for MA_Type are:

T_ROOT
A menu root object, also known as a menu strip as it is the parent of all menu objects. This differs from the old-style menu system, where a "menu strip" was just a linked list of Menu structures. Besides being the actual root of a menu tree, a menu root object also holds essential state information on the tree as a whole. It may be used in any place an old-style menu strip can, except for the ItemAddress() function (replaced by the MM_FINDID method). A menu root object can have any number of menu objects as children.
T_MENU
A menu object, equivalent to the old-style Menu structure. A menu object can have any number of menu item objects as children (and should always have at least one).
T_ITEM
A menu item object, equivalent to the old-style MenuItem structure. A menu item object can have any number of menu item objects as children (sub-items), even in the case it is itself a sub-item of some other menu item. Only a menu (sub-)item having no children (i.e. a leaf item) can be actually selected by the user.

Compatibility

Save where explicitly allowed, menu and menu item objects instantiated from menuclass cannot be transparently used in place of the equivalent old-style Intuition elements, as not all of the traditional menu-related functions and rules apply to them.

A limited form of compatibility exists due to menuclass objects having Menu or MenuItem structures embedded in them, just like gadgetclass ones do with the (Ext)Gadget structure. However, such structures should never be directly accessed by user code unless otherwise documented.

Under most circumstances, applications should treat menuclass instances as "black boxes", the only exception to this rule currently being that it is permitted to read certain bits from the Flags field of their embedded legacy structure (see also next subsection).

Recognizing a BOOPSI menu object

Although an application normally should know if it is using BOOPSI menus, it may happen that a library or a class needs to check the type of menus passed to it by a client. This is easily done with the following test:

  if ((((struct Menu *)menu_object_ptr)->Flags & BOOPSIMENU) != 0)
    ...

If the flag BOOPSIMENU is set, the object is a menuclass instance, otherwise it is a traditional Menu or MenuItem structure.

Note
In the above test it would have been exactly the same if we had chosen to cast the object to a struct MenuItem * rather than to a struct Menu * because the Flags field is located at the same offset (and has the same size) in both structures.

Adding BOOPSI menus to an application

In this section we'll cover the basics of using the new menuclass in a typical application, and the main differences between BOOPSI and traditional menus.

The first step in adding BOOPSI menus to your application is to build a tree of menuclass objects, each with its appearance and behavior defined by attributes that can be set at NewObject() time.

Label and ID of menus and menu items

For menu and menu item objects, the application should specify at least a label string and an ID number as a minimal set of attributes.

The label is assigned via MA_Label and is the text Intuition will display to represent a menu or menu item on the screen. A special value for this attribute is the constant ML_SEPARATOR, which turns a menu item into a separator line.

The ID, passed with the MA_ID tag, is a 32-bit unsigned integer that uniquely identifies a particular menu or menu item object. This number is passed back to the application through the IDCMP mechanism whenever the user picks a menu item or asks for help on a menu or menu item.

Any number may be chosen as an ID for a menuclass object, except for 0 (zero): this constant has the alias NO_MENU_ID in <intuition/menuclass.h> and is always an invalid value for an object's MA_ID attribute. Also, when an IDCMP_MENUPICK or IDCMP_MENUHELP ExtIntuiMessage reports NO_MENU_ID in its eim_LongCode field, this is to be interpreted as "no menu selection" (i.e. it has the same meaning MENUNULL has for old-style menus).

Menu root objects, as well as menu items acting as separator lines, don't need to have an ID number. Menu root objects don't need a label string either.

Building a menu tree with menuclass

A menu tree is organized as a hierarchy of nested menuclass objects.

At the very top of the tree there is the menu root, having one or more menus as children. These make up the menu strip that appears on the screen when the user presses the right mouse button.

Each menu is the parent of one or more menu items. A menu item can have other menu items as children as well, thus forming a sub-menu. A menu item which is part of a sub-menu (also called a sub-item, i.e. the child of another menu item) can in turn have children (sub-items) of its own.

The leaf nodes of the tree are menu items or sub-items with no children. These are the actually selectable menu options.

There is no limit to the depth of the menu tree other than the available system memory.

You can append children to a menuclass object at OM_NEW or OM_SET time with the MA_AddChild tag, or by using the OM_ADDMEMBER method (more on this later).

An example of a very simple menu tree generation could be:

  Object *menustripobj;
 
  menustripobj = NewObject(NULL,"menuclass",MA_Type,T_ROOT,
                   MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_MENU,
                     MA_Label, "Project",
                     MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_ITEM,
                       MA_Label, "Open...",
                       MA_ID, MID_PROJECT_OPEN,
                       TAG_END),
                     MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_ITEM,
                       MA_Label, "Save",
                       MA_ID, MID_PROJECT_SAVE,
                       TAG_END),
                     MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_ITEM,
                       MA_Label, "Save as...",
                       MA_ID, MID_PROJECT_SAVEAS,
                       TAG_END),
                     MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_ITEM,
                       MA_Label, ML_SEPARATOR,
                       TAG_END),
                     MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_ITEM,
                       MA_Label, "About...",
                       MA_ID, MID_PROJECT_ABOUT,
                       TAG_END),
                     MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_ITEM,
                       MA_Label, ML_SEPARATOR,
                       TAG_END),
                     MA_AddChild, NewObject(NULL,"menuclass",MA_Type,T_ITEM,
                       MA_Label, "Quit",
                       MA_ID, MID_PROJECT_QUIT,
                       TAG_END),
                     TAG_END),
                   TAG_END);

If you prefer a more compact style of coding, <intuition/menuclass.h> offers a number of macros allowing to simplify the tree description a little. Using said macros, the above code could be rewritten as follows:

  Object *menustripobj;
 
  menustripobj = MStrip,
                   MA_AddChild, MTitle("Project"),
                     MA_AddChild, MItem("Open..."),
                       MA_ID, MID_PROJECT_OPEN,
                     MEnd,
                     MA_AddChild, MItem("Save"),
                       MA_ID, MID_PROJECT_SAVE,
                     MEnd,
                     MA_AddChild, MItem("Save as..."),
                       MA_ID, MID_PROJECT_SAVEAS,
                     MEnd,
                     MA_AddChild, MSeparator,
                     MEnd,
                     MA_AddChild, MItem("About..."),
                       MA_ID, MID_PROJECT_ABOUT,
                     MEnd,
                     MA_AddChild, MSeparator,
                     MEnd,
                     MA_AddChild, MItem("Quit"),
                       MA_ID, MID_PROJECT_QUIT,
                     MEnd,
                   MEnd,
                 MEnd;
Note
When using the MTitle() and MItem() macros, the label is passed as an argument to the macro itself rather than via an explicit MA_Label tag.

We'll use the compact style in the rest of this document, except in cases where the more verbose one provides greater clarity.

Menu item keyboard shortcuts

As with traditional Intuition menus, menuclass item objects can have a keyboard shortcut which is displayed next to the item's label. Shortcuts can have one or more characters; a shortcut of more than one character is also called "command string" (for instance, "ctrl z").

A single-character shortcut is processed directly by Intuition, and appears at the right side of the menu item with an "Amiga key" symbol prepended to it. When using a command string of more than one character, however, Intuition will display it at the right side of the item (without the "Amiga key" symbol) but won't process the corresponding key combination; the application has to handle that on its own.

There are two ways to specify a keyboard shortcut for a menuclass item object.

You can pass it with the MA_Key attribute, as in the following example:

  item = MItem("Save"),
           MA_ID, MID_SAVE,
           MA_Key, "S",
         MEnd;

Alternatively, you can prepend it to the item's label string, separed by a '|' character, this way:

  item = MItem("S|Save"),
           MA_ID, MID_SAVE,
         MEnd;

If you opt for the latter approach, you may also use a NUL ('\0') character in place of the '|'. This can prove useful when converting to BOOPSI menu usage an existing application employing such a method to embed shortcuts in its catalog strings.

Note
The NUL byte solution only works for single-letter shortcuts and requires that no item in the menu tree has a label of just one letter, as any such label would be mistaken for a shortcut followed by random bytes. When you have single-letter labels, therefore, you cannot use this feature and must disable it for the whole tree. That's done by passing { MA_EmbeddedKey, FALSE } to the menu root object; this attribute will be inherited by all objects.

Inheritance of menu attributes

There are some attributes of menuclass objects that are automatically inherited by all their descendants, except those for which they're explicitly set to some different value. For instance, if you want your menus to use a particular font, you only need to set the MA_TextAttr attribute for the menu root, and all items (and sub-items) of all menus will be displayed with that font.

Any menuclass attribute which is automatically inherited from the parent object when not specified explicitly is clearly described as such, both in the autodoc and in the <intuition/menuclass.h> header file.

Attaching the menu tree to a window

You can attach a menu tree built with menuclass to a window just like you would do with traditional non-BOOPSI menus, i.e. by calling SetMenuStrip(). Starting with Intuition V54, this function accepts a BOOPSI menu root object as the menu strip pointer. Therefore, once you've opened your window, you can simply do the following:

  SetMenuStrip(window,menustripobj);

and the menu tree will be ready to use, after undergoing an automatic layout to adapt itself to the window's screen.

An alternative to the above is passing the menus to the window at opening time, via the new V54 tag WA_MenuStrip. This way your window will open with the menus already attached and laid out.

When using window.class, you can achieve the same by passing the class-specific tag WINDOW_MenuStrip. (Starting with V54, window.class supports WA_MenuStrip as well, as an alias for WINDOW_MenuStrip.)

Detaching the menu tree from a window

BOOPSI menus, like traditional ones, can be detached from a window at any time with ClearMenuStrip(). After having been detached, they can also be reattached by way of ResetMenuStrip(), as usual.

In many cases, however, you don't need to remove BOOPSI menus as you would with old-style menus. Changing the selection state of "checkmarkable" items, as well as disabling or enabling single items or whole menus, may be performed by just setting the appropriate attribute of the menu object in question, regardless of whether or not the menus are in use. Any needed synchronization with Intuition is handled internally by menuclass.

The above is generally true for all operations you can do on menuclass objects, such as changing their label, image, font or charset, or even their position in the menu tree. In fact, a BOOPSI menu tree even supports addition or removal of menus and items on-the-fly without any clearing and resetting of the menus.

Whenever a change requires a relayout of some part of the menu tree, menuclass will automatically take care of that.

Additionally, as of Intuition V54 it is no longer mandatory to remove the menus with ClearMenuStrip() before closing the window. CloseWindow() itself will take care of clearing the menus if needed (no matter if BOOPSI or old-style).

Note
It is still required, however, to use ClearMenuStrip() to remove the menu strip of a window before setting a different one with SetMenuStrip().

Handling menu item selections by the user

Upon receiving a menu pick event such as IDCMP_MENUPICK (or WMHI_MENUPICK if using window.class), your application can find out which menu items the user selected by invoking repeatedly the MM_NEXTSELECT method on the root object of the menu tree (i.e. the menu strip object). This will return all selected item IDs in sequence; in many cases there will be only one, but there may be more if the user performed multiple selection.

Once all IDs have been retrieved, MM_NEXTSELECT will return NO_MENU_ID which means there are no more selections in the list.

Note that MM_NEXTSELECT might even return NO_MENU_ID immediately on the first invocation; this would mean the user initiated and terminated a menu session without picking any item. Be prepared to handle this case.

The MM_NEXTSELECT method uses the following mpNextSelect message structure:

  struct mpNextSelect
  {
     uint32 MethodID;
     uint32 mpns_Reserved;
     uint32 mpns_CurrentID;
  };

where MethodID is MM_NEXTSELECT, mpns_Reserved should be always set to zero, and mpns_CurrentID is the menu item ID the method returned on the previous invocation. To obtain the first ID in the selection list, pass NO_MENU_ID in the mpns_CurrentID field.

An example of this method's usage would be:

  uint32 id = NO_MENU_ID;
 
  while ((id = IDoMethod(menustripobj,MM_NEXTSELECT,0,id)) != NO_MENU_ID)
  {
    switch (id)
    {
      /* Process menu selections */
 
      case MID_PROJECT_OPEN:
        ...
    }
  }

An alternative way to get the ID of the first (or only) selected menu item is to read it from the eim_LongCode field of the ExtIntuiMessage reporting the IDCMP_MENUPICK event (IntuiMessages generated by Intuition are always really ExtIntuiMessages, and can be cast as such). Since a menuclass object's ID is a 32-bit value, it can't fit in the 16-bit IntuiMessage.Code field, the value of which is currently undefined for IDCMP events coming from BOOPSI menus.

The above example could thus be modified as follows:

  uint32 id = ((struct ExtIntuiMessage *)intuimsg)->eim_LongCode;
 
  while (id != NO_MENU_ID)
  {
    switch (id)
    {
      /* Process menu selections */
 
      case MID_PROJECT_OPEN:
        ...
    }
 
    id = IDoMethod(menustripobj,MM_NEXTSELECT,0,id);
  }

The first approach is probably more convenient when using window.class since in that case you would need an additional IDCMP hook to access the ExtIntuiMessage in order to read its eim_LongCode field.

Handling menu help requests by the user

If your window has the WA_MenuHelp attribute set to TRUE, and it's listening to IDCMP_MENUHELP events, the user can request help on a menu item or a menu title by pressing the [Help] key when that item or title is being highlighted by the mouse pointer. Upon reception of a menu help event, your application can easily find out the ID of the item or title the help request was about by querying the MA_MenuHelpID attribute of the menu root object.

An example of this might be:

  uint32 help_id;
 
  GetAttr(MA_MenuHelpID,menustripobj,&help_id);

Note that the menu help ID value may be NO_MENU_ID, which means the [Help] key was pressed by the user while no menu item or title was highlighted.

Menu help doesn't support multiple selection, so you have to deal with just one ID per event. As with menu pick, you can also retrieve the ID value by reading the ExtIntuiMessage.eim_LongCode field.

Menu pick hooks and menu help hooks

Any menuclass object can have custom hooks associated to it with dedicated code to handle menu selections or help requests for that specific object. Such hooks can be invoked automatically by the main event handling loop every time a menu pick or menu help event occurs.

The hook that handles menu item selections is specified through the MA_PickHook attribute; similarly, you can use the MA_HelpHook attribute to specify the hook handling menu help requests.

These two attributes are inherited by all children of a menuclass object. This allows, if desired, to reuse the same hook for all the items of a menu (or all the sub-items of an item) simply by setting it for that menu (or item), rather than having to pass it to each child explicitly. Such a "common" hook could act as a dispatcher, calling the appropriate individual handling function according to the ID of the object it is invoked on.

A menuclass object's pick or help hook will be invoked as follows:

  HookFunction(struct Hook *hook, Object *obj, struct MenuEventMessage *msg)

where 'obj' is the object itself and 'msg' is a pointer to a MenuEventMessage structure.

The MenuEventMessage structure is defined as follows:

  struct MenuEventMessage
  {
      uint32 StructSize;      /* For future expansion */
      uint32 EventType;       /* ET_MENUPICK or ET_MENUHELP */
      struct Window *Window;  /* Event window pointer */
      APTR UserData;          /* Custom data pointer */
  };

The EventType field is useful to distinguish between the two types of event the hook could be invoked for, in case the same hook function is used for both. The Window field holds the address of the event's window (i.e. the window your menu tree is associated to) and the UserData field is used to pass to the hook any kind of context information it might require, as explained a little below.

The pick or help hook can use the supplied information (menu object, window and custom data pointer) to process the event. The return value of the hook is not currently defined; it's recommended to set it to zero for future compatibility.

To have menuclass invoke the pick hook of a menuclass object, your application needs to use the MM_HANDLEPICK method in its event loop.

Like MM_NEXTSELECT, MM_HANDLEPICK can be invoked repeatedly to browse through all items in the menu selection chain. The difference is that this method only returns the ID of selected items that don't have a pick hook; whenever it finds a selected item whose pick hook is non-NULL, it invokes that hook and proceeds to the next item without returning anything. When there are no more items left in the menu selection chain, MM_HANDLEPICK returns NO_MENU_ID.

The mechanism is exactly the same for help hooks, except in that case you have to use the MM_HANDLEHELP method, which only needs to invoked once as menu help events don't support multiple selection: if the menu object the user asked help about doesn't have a help hook, MM_HANDLEHELP will return its ID, otherwise it will invoke the hook and return NO_MENU_ID.

Both the MM_HANDLEPICK and MM_HANDLEHELP methods should be invoked on the menu root object, and use the same message structure:

  struct mpHandleEvent
  {
      uint32 MethodID;
      uint32 mphe_Reserved;
      uint32 mphe_CurrentID;
      struct Window *mphe_Window;
      APTR mphe_UserData;
  };

where MethodID is MM_HANDLEPICK or MM_HANDLEHELP, mphe_Reserved should always be set to zero, and mphe_CurrentID is the ID returned by the method's previous invocation (ignored by MM_HANDLEHELP; pass NO_MENU_ID on the first invocation of MM_HANDLEPICK). Additionally, you should store the event's window address in mphe_Window (e.g. IntuiMessage->IDCMPWindow) and any custom data to be passed to the pick or help hook in mphe_UserData; your hook will be able to read back this information from the MenuEventMessage.

A simple example of how to take advantage of the custom pick hook feature might be the following:

  uint32 id = NO_MENU_ID;
 
  while ((id = IDoMethod(menustripobj,MM_HANDLEPICK,
                                      0,
                                      id,
                                      imsg->IDCMPWindow,
                                      mycustomdata)) != NO_MENU_ID)
  {
    /* This selected menu object doesn't have
     * a pick hook, so let's handle it here.
     */
    switch (id)
    {
      /* Process menu selections */
 
      case MID_PROJECT_OPEN:
        ...
    }
  }

It goes without saying that if none of your application's menuclass object has a pick hook or help hook, you never need to invoke MM_HANDLEPICK/MM_HANDLEHELP in your event handling code; you can just use the simpler MM_NEXTSELECT method and MA_MenuHelpID attribute instead, as covered in previous subsections.

Retrieving a menuclass object's address from its ID number

Once you have found out which menu item was picked by the user by examining its ID, you may want to do some operations on the item object itself, and thus need its actual address. One way to achieve this could be to keep a look-up table in your application, initialized as the menu tree gets built, and using the IDs as indices. For instance, something like the following:

  ...
  MA_AddChild, mtable[MID_PROJECT_OPEN] = MItem("Open..."),
    MA_ID, MID_PROJECT_OPEN,
  MEnd,
  MA_AddChild, mtable[MID_PROJECT_SAVE] = MItem("Save"),
    MA_ID, MID_PROJECT_SAVE,
  MEnd,
  ...

The drawbacks of such an approach are increased memory consumption and reduced code readability. A simpler solution is offered by menuclass in the form of the MM_FINDID method. This uses the mpFindID structure, defined as:

  struct mpFindID
  {
      uint32 MethodID;
      uint32 mpfi_Reserved;
      uint32 mpfi_ID;
  };

where MethodID is MM_FINDID, mpfi_Reserved should always be set to zero, while mpfi_ID is the ID of the menu object whose address you're looking for. Here's a simple example of this method's invocation on the menu root object:

  m_obj = (Object *)IDoMethod(menustripobj,MM_FINDID,0,id);

If a menu object with the specified ID is found within the menu tree, MM_FINDID will return its address, otherwise it will return NULL.

The MM_FINDID method doesn't actually need to be invoked on the menu root as in the above example. It just searches for an object having a given ID within the menu (sub-)tree hanging from the object it's invoked on. So if you already know the address of an object whose local sub-tree contains the one you want, it is possible to invoke MM_FINDID directly on that object and restrict the search to only a part of the menu tree.

Invoking the MM_FINDID method is the BOOPSI equivalent of calling ItemAddress() with traditional menus.

Enabling and disabling menus and menu items

By default, a BOOPSI menu or item is enabled, meaning it looks clearly readable and can be selected by the user. When disabled, it is instead rendered with low contrast or a recessed appearance and is not selectable.

The MA_Disabled attribute is used to control the enable state of BOOPSI menu or item objects. It is possible to disable a menuclass object right from the start by passing { MA_Disabled, TRUE } to NewObject(), or change the object's enable state later at any time via SetAttrs(). Setting MA_Disabled to TRUE or FALSE is the BOOPSI equivalent of calling OnMenu() or OffMenu() on old-style menus.

Disabling a menu or a menu item will make all of its children show up disabled as well, down to the deepest sub-menu levels. Additionally, you can disable all your menus at once (i.e. make the entire menu tree disabled and not selectable) simply by disabling the menu strip object.

Another way to examine and modify the enable state of a menu or menu item is to invoke the MM_GETSTATE and MM_SETSTATE methods (see below for more on this).

Selection state of toggle select and mutual exclude items

Whenever a toggle select or mutual exclude menu item is picked by the user, you can examine its selection state by reading the item's MA_Selected attribute. If its value is TRUE, the item is currently selected (checked) and is displayed with some small checkmark or radio button symbol drawn in front of it.

You can also programmatically change the selection state of this kind of items at any time by setting the value of MA_Selected with SetAttrs(), without having to clear and then reset the menu strip as you would with non-BOOPSI menus.

This attribute has no meaning for action items, i.e. items that don't represent the "on" or "off" state of some option.

Another way to examine and modify the selection state of a menu or menu item is to invoke the MM_GETSTATE and MM_SETSTATE methods (see below for more on this).

Methods for controlling the enable and selection state of menu items

It is possible to examine and modify the enable state of a menu or menu item by invoking the MM_GETSTATE and MM_SETSTATE methods. These may prove more handy in many cases since they take an ID number to specify the target object instead of the object's actual address. This means they can be directly fed the result of MM_NEXTSELECT without having to go through MM_FINDID first.

Both these methods use the bitmask constants MS_CHECKED and MS_DISABLED to read or modify the corresponding MA_Selected and MA_Disabled attributes.

The MM_GETSTATE method uses the following message structure:

  struct mpGetState
  {
      uint32 MethodID;
      uint32 mpgs_Reserved;
      uint32 mpgs_ID;
  };

where MethodID is MM_GETSTATE, mpgs_Reserved should always be set to zero, and mpgs_ID is the ID number of the menuclass object whose state you want to read. The method should be invoked on the menu root object (or on the parent of the target object, if you have its address) and will return a bit mask whose value can be MS_CHECKED, MS_DISABLED, both constants ORed together, or zero. For instance, if MS_CHECKED is set in the method's return value it means that the object's MA_Selected attribute is currently TRUE.

The MM_SETSTATE method uses the following message structure:

  struct mpSetState
  {
      uint32 MethodID;
      uint32 mpss_Reserved;
      uint32 mpss_ID;
      uint32 mpss_ApplyMask;
      uint32 mpss_StateMask;
  };

where the first three fields are analogous to those of the mpGetState structure while mpss_ApplyMask is used to specify which attribute(s) should be modified, and mpss_StateMask contains the new values. For instance, if MS_DISABLED is set in mpss_ApplyMask, but not in mpss_StateMask, the object will become enabled as its MA_Disabled attribute will be set to FALSE. If MS_CHECKED is cleared in the mpss_ApplyMask field, on the other hand, the object's selection state will stay unchanged no matter what the value of mpss_StateMask is.

Disposing of the menu tree

Due to its dynamic nature, usually a BOOPSI menu tree doesn't need to be freed and rebuilt by your application on environment changes or each time it has to undergo some significant modification. You may for instance reuse the same menu tree on different screens (as long as it's used on one screen at a time).

This means that all important state information stored in the menu tree such as which items are checked, disabled, etc. is never lost while the application is running, and that the menu tree can be disposed of just once on program's exit.

To free an entire menu tree, you just have to call DisposeObject() on its root object. Whatever disposable object you attached to some menu item, such as e.g. a BOOPSI image, will be freed as well unless you asked not to do so.

Further menuclass features

This section will outline a number of attributes and methods giving access to some more specialized functionality of menuclass.

Specifying the menu font

Each menu item object can be displayed in its own font and size, although this is strongly discouraged as such an arrangement would look quite unprofessional. Still, if you for any reason need to set the font used by a menu item, you can do so with the MA_TextAttr attribute.

The font specified this way will be opened and closed by menuclass as needed.

If you set MA_TextAttr for a menuclass object, it will be inherited by all of its children. To specify a font for the entire menu tree, therefore, just set MA_TextAttr for the menu root object.

Whenever there isn't any specific reason to do otherwise, it's recommended not to use MA_TextAttr at all and just let the menus be displayed in the font that was chosen in the user's preferences for the screen they appear on.

Adding images to menu items

A BOOPSI menu item can have an image displayed at the left side of (or in place of) the item's label. The image is specified through the MA_Image attribute; it may be either traditional or BOOPSI.

The item's height is adjusted if needed to make enough room for the image.

If the image is BOOPSI, it will be automatically disposed of when the menu item object is; to avoid that, you should set the item's MA_FreeImage attribute to FALSE. However, if you want to prevent automatic disposal for ALL images in the menu tree, it is more practical to set MA_FreeImage to FALSE for the menu root object since this way it will be inherited by all objects.

If the menu item has no label (i.e. it's an image-only item), you can still use MA_TextAttr to specify the font used for its keyboard shortcut character.

Hiding menus and menu items

Any member of a BOOPSI menu tree can be hidden or revealed again at any time by your application. A hidden menuclass object is still physically present in the menu tree, but does not appear on the screen when menus are displayed, as if it had been actually removed.

The MA_Hidden attribute is used to control the hiding of a BOOPSI menu or item objects. It is possible to make a menu or item hidden right from the start by passing { MA_Hidden, TRUE } to NewObject(), or change the object's hiding state later via SetAttrs().

Hiding a menu or a menu item will (obviously) hide all of its children as well. Unlike disabling, however, you cannot hide the menu strip object -- if you want to make the entire menu tree disappear, just call ClearMenuStrip().

Dynamic menu localization

So far, we've only examined the simple case where you pass an actual string as the value of a menuclass object's MA_Label attribute. There is however another possibility, and that is to specify a numeric index as MA_Label's value, which is later used to look up the appropriate string in the currently open catalog.

To be able to pass an integer value for MA_Label instead of a string, you need to tell menuclass what is the numeric range of string IDs. Any value outside of that range will be considered a string, and used directly as such. Values lying in the range, on the other hand, will be treated as catalog indices. The string ID range is specified with the MA_MinStringID and MA_MaxStringID attributes. (By default, the string ID range is empty and MA_Label is always interpreted as a string.)

Whenever an object's MA_Label attribute is actually a numeric index, its value is passed on each menu layout to a "localization hook" which will convert it to the correct string for the current language and charset.

The localization hook (aka string hook) is supplied by your application via the MA_StringHook attribute. You can simply set it for the root object, and it will be inherited by all objects in the menu tree.

Your hook will be invoked as follows:

  HookFunction(struct Hook *hook, Object *obj, struct MenuStringMessage *msg)

where 'obj' is the menu or item the string ID to be converted belongs to, and 'msg' is a pointer to a MenuStringMessage structure (see below for definition) which carries the string ID.

You can also specify the address of an open catalog and/or a charset by setting the MA_Catalog and MA_CharSet attributes for the menu root object; if supplied, this information is also passed back to the hook in the MenuStringMessage.

The MenuStringMessage structure is defined as follows:

struct MenuStringMessage
{
    uint32 StructSize;        /* For future expansion */
    uint32 StringID;          /* The string ID number */
    struct Catalog *Catalog;  /* Catalog pointer, may be NULL */
    uint32 CharSet;           /* Charset number, may be zero */
};

Your string hook should return the correct catalog string for the passed string ID, or a default string if no catalog is available.

Note: all of the above is equally valid for the MA_Key attribute, which can be a string ID instead of an actual string just like MA_Label. Its string ID range is the same one of MA_Label (as defined by MA_MinStringID and MA_MaxStringID).

Also note: if you don't use a string hook, MA_Label and MA_Key should always be real strings and not numeric IDs, otherwise they'll get converted by menuclass to "????", which isn't very meaningful. The same will happen if the string hook returns a NULL string pointer.

Adding and removing menus and menu items

A BOOPSI menu tree built with menuclass allows for dynamic addition and removal of menus and items at any time. As with other modifications, there's no need to detach the menu tree from the window in order to perform these operations; it's menuclass that takes care of synchronizing each change with Intuition and doing any necessary relayout afterwards.

To add a child to a menuclass object, you can simply call SetAttrs() to pass it the MA_AddChild tag with the address of the new child as data, the same way you would at OM_NEW time.

Here's an example of adding a sub-item to an item:

  /* Append a new entry to our "recent files" sub-menu */
 
  new_item = MItem(filename), MA_ID, new_id++, MEnd;
 
  SetAttrs(recentfiles_itemobj,MA_AddChild,new_item,TAG_END);

A NULL pointer can safely be passed with MA_AddChild, and is simply ignored.

Note that in this example we're using a simple counter variable to generate an unique ID for each new sub-item -- we don't care about the ID value itself, we simply want to take notice if the item gets selected, and be able to retrieve its address with MM_FINDID whenever needed. This requires the new item to have a valid and unique ID number.

Just like they can be added, menus and menu items can also be removed from the menu tree as needed. This is achieved by passing the MA_RemoveChild tag to the parent menu object via SetAttrs(), with the address of the child as data.

For instance, we could remove an item from a menu this way:

  /* Close a window and remove its entry from our "open windows" menu */
 
  id = (uint32)window->UserData;  /* We stored the menu item's ID there */
 
  CloseWindow(window);
 
  item = (Object *)IDoMethod(openwindows_menuobj,MM_FINDID,0,id);
 
  SetAttrs(openwindows_menuobj,MA_RemoveChild,item,TAG_END);
 
  DisposeObject(item);  /* Accepts a NULL argument */

As with MA_AddChild, you can safely pass a NULL pointer with MA_RemoveChild.

Another way to perform addition and removal of children with menuclass objects is to use rootclass' OM_ADDMEMBER and OM_REMMEMBER methods. The above examples could thus be rewritten by replacing the SetAttrs() calls with:

  IDoMethod(recentfiles_itemobj,OM_ADDMEMBER,new_item);
 
  IDoMethod(openwindows_menuobj,OM_REMMEMBER,item);

As you would when dealing with Exec lists, you must be careful to avoid adding or removing the same child twice, or removing a child which was never added in the first place, since doing so will cause memory corruption.

Browsing through all children of a menu object

You can obtain the addresses of all children of a menuclass object in sequence by invoking the MM_NEXTCHILD method in a loop. Once all the object's children have been been retrieved, MM_NEXTCHILD will return NULL to signal there are no more children.

The MM_NEXTCHILD method uses the following message structure:

  struct mpNextChild
  {
    uint32 MethodID;
    uint32 mpnc_Reserved;
    Object *mpnc_Current;
  };

where MethodID is MM_NEXTCHILD, mpnc_Reserved should always be set to zero, and mpnc_Current is the child object address the method did return on the previous invocation. Pass NULL in mpnc_Current to get the address of the first child.

An example of a loop retrieving all items of a menu:

  Object *obj = NULL;
 
  while ((obj = (Object *)IDoMethod(menuobj,MM_NEXTCHILD,0,obj)) != NULL)
  {
    /* Do something with the item object */
  }

If you use MM_NEXTCHILD in a loop to remove an object's children one by one, be careful not to pass the method the address of an already removed object.

The correct way to do a sequential child removal is the following:

  while ((obj = (Object *)IDoMethod(menuobj,MM_NEXTCHILD,0,NULL)) != NULL)
  {
     IDoMethod(menuobj,OM_REMMEMBER,obj);
  }

This is analogous to performing a RemHead() loop on an Exec list.

Scanning the menu tree

You can perform a scan of the whole menu tree, or even just a subsection of it, and have a custom function called on each object that is part of it. This makes it possible to implement any kind of operation that is not already provided by the existing menuclass methods.

To scan a menu (sub-)tree, you invoke the MM_SCAN method on its root object and specify a callback function and some parameters that will be passed back to it on each call. The MM_SCAN method uses the following message structure:

  struct mpScan
  {
    uint32 MethodID;
    uint32 mps_Reserved;
    struct Hook *mps_Hook;
    uint32 mps_Args[4];
  };

where MethodID is MM_SCAN, mps_Reserved should always be set to zero, mps_Hook is a pointer to your custom hook, and mps_Args[0] to mps_Args[3] are (optional) arguments for the hook's function. Your hook will be invoked as follows:

  HookFunction(struct Hook *hook, Object *obj, struct MenuScanMessage *msg)

where 'obj' is a menuclass object (whose exact type you can inspect by reading its MA_Type attribute) and 'msg' is a pointer to a MenuScanMessage structure.

The MenuScanMessage structure is defined as follows:

  struct MenuScanMessage
  {
    uint32 StructSize;  /* For future expansion */
    int32 Level;        /* How deep we are in the menu tree */
    uint32 Args[4];     /* Custom arguments */
  };

The Level field indicates the depth in the tree of the current object, starting from -1 for the root object. The four Args variables hold the same values which were passed upon invocation of the MM_SCAN method and can be used to feed your custom arguments back to the hook.

If your hook returns a non-zero value, the scan stops with the method returning the address of the object on which it stopped. If the hook always returns zero, all objects are scanned and the MM_SCAN method returns NULL.

An example of this method's usage could be the following:

  /* A simple function to count how many items a menu has, using MM_SCAN */
 
  uint32 HookFunction(struct Hook *h, Object *o, struct MenuScanMessage *msg)
  {
     if (msg->Level == 1)
        *((uint32 *)msg->Args[0]) += 1;
     return (0);
  }
 
  uint32 CountMenuItems(Object *menuobj)
  {
     struct Hook hook;
     uint32 count = 0;
 
     memset(&hook,0,sizeof(struct Hook));
 
     hook.h_Entry = (HOOKFUNC)HookFunction;
 
     IDoMethod(menuobj,MM_SCAN,0,&hook,&count,0,0,0);
 
     return (count);
  }

In this example we pass as the only custom argument the address of an variable which, once the MM_SCAN method returns, will hold the amount of children of the specified menu object. In the hook function we check that the object's level is 1, to make sure we only take it into account if it is a direct child of a menu (menus have a level of zero), rather than being a sub-item of an item, or even the menu object itself on which the method is invoked.

Generating a whole menu (sub-)tree from a single tag list

So far we have only seen menu trees built by way of a series of nested calls to NewObject(). This is certainly a working approach to putting together a tree of BOOPSI objects organized in a hierarchy. Still, it presents a few drawbacks:

  • It can consume a significant amount of stack with very deep trees;
  • It may produce code which is hard to read unless special macros are used;
  • It makes error checking not very easy.

All in all, nesting NewObject() calls is quite acceptable with relatively small menu trees, while it can become less manageable as the tree's size grows.

A different technique for building menu trees, which attempts to overcome these issues, is provided by menuclass' MM_NEWMENU method. This allows to generate an entire tree, or a sub-tree, out of a flat description of its structure given in the form of a single tag list.

The MM_NEWMENU method uses the following message structure:

  struct mpNewMenu
  {
    uint32 MethodID;
    uint32 mpnm_Reserved;
    struct TagItem *mpnm_AttrList;
  };

where MethodID is MM_NEWMENU, mpnm_Reserved should always be set to zero, and mpnm_AttrList is the tag list describing the menu tree to be generated.

Said tag list is made up for the most part of the usual menuclass tags that are used to specify attributes of the individual objects; however, the menu objects themselves are introduced by special tags prepended with NM_ which are actually directives for the method.

MM_NEWMENU understands three directives: NM_Menu / NM_Item, indicating to add a new menu or item object at the current level (the tag data is the object's text label), and NM_SubItems, indicating to move down or up by one level before the next item object is added. If the tag data of NM_SubItems is SI_BEGIN, the next item will become a child of the most recent one added (i.e. we move down by one level); if it is SI_END, the next item will become a sibling of the most recent one's parent (i.e. we move up by one level).

Note that NM_SubItems is not needed to add menus to a menu strip object, nor to add items to a menu object -- its only purpose is to act as a "delimiter" for a list of sub-items of an item object.

Any menuclass tag that is encountered in the tag list is passed as an attribute to the most recently added object.

The method should be invoked on an existing menuclass object, which in case of success will become the new tree's root. MM_NEWMENU returns FALSE to indicate failure, the exact reasons for which can be inspected through the MA_ErrorCode and MA_ErrorTagItem tags. See <intuition/menuclass.h> for a list of menuclass error codes and their explanation.

Here's a simple example to illustrate the method's usage:

  /* Service function allowing to pass varargs tag list to MM_NEWMENU
   */
  static BOOL VARARGS68K DoNewMenu(Object *root, ...)
  {
    va_list ap;
    struct TagItem *tags;
    BOOL result;
 
    va_startlinear(ap,root);
    tags = (struct TagItem *)va_getlinearva(ap,struct TagItem *);
    result = IDoMethod(root,MM_NEWMENU,0,tags);
    va_end(ap);
 
    return (result);
  }
 
  /* Build a menu tree with the MM_NEWMENU method
   */
  Object *BuildMenuTree(void)
  {
    Object *menustripobj;
    uint32 success, error;
    struct TagItem *error_ti;
 
    menustripobj = NewObject(NULL,"menuclass",MA_Type,T_ROOT,TAG_END);
 
    if (menustripobj)
    {
      success = DoNewMenu(menustripobj,
        MA_ErrorCode, &error,
        MA_ErrorTagItem, &error_ti,
        NM_Menu, "Project",         MA_ID, MID_PROJECT,
          NM_Item, "O|Open",        MA_ID, MID_OPEN,
          NM_Item, "S|Save",        MA_ID, MID_SAVE,
          NM_Item, "A|Save as...",  MA_ID, MID_SAVEAS,
          NM_Item, ML_SEPARATOR,
          NM_Item, "Q|Quit",        MA_ID, MID_QUIT,
        NM_Menu, "Options",         MA_ID, MID_OPTIONS,
          NM_Item, "F|Font...",     MA_ID, MID_FONT,
          NM_Item, "Style",         MA_ID, MID_STYLE,
            NM_SubItems, SI_BEGIN,
            NM_Item, "B|Bold",      MA_ID, MID_BOLD,      MA_Toggle, TRUE,
            NM_Item, "I|Italic",    MA_ID, MID_ITALIC,    MA_Toggle, TRUE,
            NM_Item, "U|Underline", MA_ID, MID_UNDERLINE, MA_Toggle, TRUE,
            NM_SubItems, SI_END,
          NM_Item, "W|Word wrap",   MA_ID, MID_WORDWRAP,  MA_Toggle, TRUE,
        TAG_END);
 
      if (!success)
      {
        Printf("Failed with error %ld, at tag %08lX\n",error,error_ti->ti_Tag);
        IDoMethod(menustripobj,MM_DELETEMENU,0);
        DisposeObject(menustripobj);
        menustripobj = NULL;
      }
    }
 
    return (menustripobj);
  }

In this example we make use of a small service function to pass the tag list on the stack as varargs; of course the tag list can also be allocated statically. Also, the above tree is rather small for the sake of keeping the example short, so it does not benefit much from using MM_NEWMENU instead of nested NewObject() calls, but obviously the same technique can be used to build trees of any size.

As you can see, with MM_NEWMENU you can write compact and quite readable code to describe your menu trees, without having to resort to using macros on top of nested calls. The tree's description can even be made somewhat reminiscent of traditional NewMenu arrays as used with GadTools, which may look more familiar to long-time developers.

To free a menu (sub-)tree generated with MM_NEWMENU, you invoke MM_DELETEMENU on the root object. This method uses the following message structure:

  struct mpDeleteMenu
  {
    uint32 MethodID;
    uint32 mpdm_Reserved;
  };

where MethodID is MM_DELETEMENU, and mpdm_Reserved should (as usual) be set to zero. Therefore, the correct way to invoke the method is the following:

  IDoMethod(menustripobj,MM_DELETEMENU,0);

Note that this call only disposes of all of the object's children, not of the root object itself. This way you can reuse the object in case you need to build another menu tree with MM_NEWMENU. To free the root object (and its children if you didn't already do that), simply use DisposeObject().