Copyright (c) Hyperion Entertainment and contributors.

Intuition Context Menus

From AmigaOS Documentation Wiki
Jump to navigation Jump to search

An overview

Starting with version 54.6, Intuition makes it possible to add context menus to windows and gadgets. A context menu is a menu offering a limited set of choices that apply specifically to a particular element of the application, rather than to the application as a whole. Furthermore, a context menu can be brought up by the user only when the element it applies to lies under the mouse pointer, and it always pops up near, if not over, the element itself.

From an application's viewpoint, a context menu is a menu strip which contains just one menu, having as many items and sub-items as desired. It can be either of the traditional type, or of the BOOPSI type (i.e. built with menuclass).

An Intuition context menu can be tied either to some zone of the window or to a gadget. When the user presses the right mouse button while the mouse pointer is over that zone or gadget, the context menu appears under the pointer and allows the user to select some action or option which is meaningful for the object the menu is tied to.

If the RMB is pressed at coordinates that don't correspond to any context menu, the normal window menus (if any exist) will come up as usual.

Context menus tied to a window zone

You can enable context menus for a window by way of a context menu hook. That's passed to the window with the WA_ContextMenuHook tag, and is invoked each time Intuition is about to bring up the menus of a window in response to a RMB click by the user.

By examining the current mouse position, the hook can decide whether to return a specific context menu which will pop up under the mouse pointer, or NULL to get the window's normal menu strip.

If a context menu is actually passed back to Intuition, any event from it will be sent to the application as an IDCMP_MENUPICK or IDCMP_MENUHELP IntuiMessage, as usual. The application is able to check whether a menu event comes from a context menu, and also obtain all the needed information to process the event correctly, by reading the additional tag list attached to the IntuiMessage. (Later on in this document you'll find a list of menu event tags along with an explanation of their usage.)

The hook's function gets called with the window as object, and a ContextMenuMsg structure as message. This structure is defined as follows:

  struct ContextMenuMsg
  {
    uint32 State;  /* CM_QUERY */
 
    /* Set the following fields on CM_QUERY */
 
    APTR Menu;     /* A context menu, or NULL for the window menu */
    APTR Context;  /* The application-specific element the menu is tied to */
  };

The State field of the message tells the hook what action it is being invoked for; currently this can only be CM_QUERY, which means "supply a context menu pointer to Intuition".

When the state is CM_QUERY, the hook function must fill in the Menu field with a menu pointer (either a BOOPSI "menuclass" object tree or a traditional Menu structure with its chain of items and sub-items).

It can also fill in the Context field with some unique value (e.g. an address) that identifies the application-specific element the returned context menu is being brought up for; the application can read this value back when receiving an IDCMP_MENUPICK or IDCMP_MENUHELP message from the context menu, in order to know exactly what element the context menu selection applies to. That's useful since the context menu hook may return the same context menu for more than one element, i.e. its choice of context menu may depend exclusively on the TYPE of the element lying under the mouse pointer at the time the RMB is pressed.

The function can read the current mouse position from the MouseX/MouseY fields of the passed window structure to decide what context menu to pass back in the Menu field. Returning NULL as menu is valid, and just makes Intuition bring up the window's normal menu strip.

The context menu returned by the hook may have been set up by the application at start-up time and stored somewhere for the hook to use, or it may even get built on-the-fly by the hook itself, in case a dynamic context menu is needed. Any context menus built by the hook should be cached, and freed later by the application (typically on exit).

The hook's return value is currently ignored, but it is recommended to always set it to zero for future compatibility.

Context menus tied to a gadget

Gadgets can have a context menu attached to them in one of two possible ways.

The simplest one is to set a gadget's GA_ContextMenu attribute to the address of a context menu set up by the application. In this case the application is fully responsible for handling any event coming from the context menu; in most cases, the gadget is not even aware of its context menu's existence.

The other way is to have the gadget itself set up and return a context menu on demand. This lets the gadget have full control on the menu's contents and even be able to return different menus depending on what part of the gadget is under the mouse pointer when the RMB is clicked. When employing this technique, menu events are handled by the gadget in a dedicated method, although they are first sent to the application as it has the responsibility of invoking said method on the gadget (this is covered in the next subsection).

Whenever the RMB is pressed while the mouse pointer is over a gadget, Intuition asks the gadget to provide, if available, a context menu to be opened in place of the normal menus. It does so by invoking GM_QUERY on the gadget, with query type GMQ_CONTEXTMENU.

When a gadget receives a GM_QUERY/GMQ_CONTEXTMENU message, it is given a chance of returning a context menu pointer in the gpq_Data field of the passed gpQuery structure. The gadget can read the mouse pointer position from gpq_IEvent and use this information to decide whether or not a context menu should be opened, and what its contents should be (each different part of the gadget may offer an individual context menu, and some parts might even have none).

Context menus returned by a gadget upon GM_QUERY may have been set up at OM_NEW time and kept in memory for the entire life of the gadget, or they may be built on-the-fly only when needed, which also allows for dynamic context menus.

If no menu is returned by this method, Intuition will check if a context menu was attached to the gadget by the application via the GA_ContextMenu attribute, and use that one if this is the case. If that check fails as well, the normal window menu strip will be displayed as usual.

If a gadget class returns a valid context menu through GM_QUERY, it should then be prepared to handle user input from the menu being fed back to it in the form of GM_MENUPICK/GM_MENUHELP messages.

The GM_MENUPICK and GM_MENUHELP methods (new for V54) are used to let a custom gadget hear context menu selections or help requests by the user, and have the chance of handling them.

These two methods use the following message structure:

  struct gpMenuEvent
  {
     uint32             MethodID;
     struct GadgetInfo *gpme_GInfo;       /* Always NULL */
     APTR               gpme_MenuAddress; /* Originating context menu */
     uint32             gpme_MenuNumber;  /* First item selected */
     struct Window     *gpme_Window;      /* The gadget's window */
  };
Note
Unlike most input methods, GM_MENUPICK and GM_MENUHELP are normally invoked on the context of the application rather than that of input.device. This is because the gadget could need to perform any sort of complex actions in response to a context menu selection, even involving disk I/O or high-level system calls that would be unfeasible on the input.device task.

For the same reason, these methods are never invoked under Intuition's state machine (e.g. via DoGadgetMethod()), since that would make them unable to call a number of Intuition functions without deadlocking. A side effect of this is that GM_MENUPICK/GM_MENUHELP are always passed a NULL GadgetInfo pointer, with the needed context information provided instead by a pointer to the gadget's window (gpme_Window). Implementors of these two input methods should therefore use DoGadgetMethod() themselves to invoke other gadget methods requiring a GadgetInfo.

The address of the context menu that originated the event can be found in the gpme_MenuAddress field of the message structure. The menu number of the first item or sub-item that was picked by the user is stored in the gpme_MenuNumber field; this number is the menu item's ID for BOOPSI (menuclass) menus, and a traditional menu code for old-style menus. If the method is GM_MENUPICK, there can be other items in the selection chain in case the user performed multiple selection.

For GM_MENUPICK, if the context menu is BOOPSI, you can ignore gpme_MenuNumber and simply read all item selections in sequence with MM_NEXTSELECT.

GM_MENUHELP, on the other hand, is always triggered for a single menu element, and there's no selection chain to follow. So, even for BOOPSI menus, reading gpme_MenuNumber is the quickest way to find out which item the user asked for help about. Alternatively, you can always query the MA_MenuHelpID attribute of the context menu's root object (pointed to by gpme_MenuAddress).

After processing the event, the gadget may free the menu, if it was generated by the gadget itself upon GM_QUERY. If this is not done, the menu will have to be cached in the gadget's instance data and freed at OM_DISPOSE time.

The return value of these two methods is currently ignored; it should always be set to zero for future compatibility.

Handling IDCMP events from context menus

Context menu events will be sent to your window's UserPort as an IDCMP_MENUPICK or IDCMP_MENUHELP IntuiMessage, just like it happens with normal menu events.

Such messages are handled mostly the same way as those that come from a window menu strip. In order to interpret them correctly, however, the application will first need to determine whether an IDCMP message is in fact caused by a context menu selection; if so, it also has to find out which context menu originated it (unless the application's GUI has just one context menu) and what element (that is, what "context") the received message actually applies to.

In order to help determine all this, IntuiMessages associated to menu events do now carry some additional information.

The IAddress field of IDCMP_MENUPICK and IDCMP_MENUHELP messages points to the specific menu strip, or context menu, the menu event is coming from. This is to allow applications to quickly get the address of the menu strip that triggered a context menu event. (In the case of an event from the window's normal menu strip, IAddress will be the same as IDCMPWindow->MenuStrip.)

Also, IDCMP_MENUPICK and IDCMP_MENUHELP IntuiMessages have a tag list attached which can be accessed by casting the message to an ExtIntuiMessage and reading its eim_TagList field. (This can always be done, since all IntuiMessages coming from Intuition are actually ExtIntuiMessages.)

The following tags are currently defined for menu-related IntuiMessages:

IMTAG_MenuType
indicates the type of the menu that triggered the event:
IMT_DEFAULT
means the window's normal menu strip.
IMT_CONTEXT_WINDOW
means a context menu originated from the window's context menu hook.
IMT_CONTEXT_GADGET_APP
means a context menu belonging to a gadget, attached to it by the application with the GA_ContextMenu attribute.
IMT_CONTEXT_GADGET_OBJ
means a context menu belonging to a gadget, generated by the gadget itself through the GM_QUERY method (see also note below).
IMTAG_MenuContext
for a context menu event, returns the actual "context" the event applies to. In case of an IMT_CONTEXT_WINDOW event, this is whatever value your window context menu hook passed back in the ContextMenuMsg.Context field. For IMT_CONTEXT_GADGET_APP and IMT_CONTEXT_GADGET_OBJ, this is the address of the gadget the menu belongs to. This tag is not present for a normal window menu strip.
Note
Whenever the originating menu is of type IMT_CONTEXT_GADGET_OBJ, the application needs to react to the event by invoking GM_MENUPICK or GM_MENUHELP (according to the IDCMP class of the IntuiMessage) on the context menu's gadget via IDoMethod(). The three arguments passed to these methods (gpme_MenuAddress, gpme_MenuNumber and gpme_Window) are the values of the IAddress, eim_LongCode and IDCMPWindow fields of the received IntuiMessage (cast it to ExtIntuiMessage to access eim_LongCode).

The reason for this special handling is to allow the gadget to process its menu selections completely on the application's context, since the processing could well involve operations which cannot be carried out on the input.device task's context (like performing disk I/O) or Intuition function calls that would cause a deadlock if executed under Intuition's state machine (for instance, a gadget might have a context menu featuring an option that brings up a file requester).

When using window.class, all of this is handled automatically so that you don't have to care about the above in your code.

Here's a simple example of how all this could be put to use:

  /* This could be a part of the event loop of an AmiDock-like program */
  /* In this example we're using BOOPSI menus to keep the code simple */
 
  switch (imsg->Class)
  {
    case IDCMP_MENUPICK:
 
      /* The ID of the first menu item in the selection chain
       */
      longcode = ((struct ExtIntuiMessage *)imsg)->eim_LongCode;
 
      /* The tag list of this IntuiMessage
       */
      taglist = ((struct ExtIntuiMessage *)imsg)->eim_TagList;
 
      /* Retrieve some useful information from the tag list
       */
      menu_type = IUtility->GetTagData(IMTAG_MenuType,IMT_DEFAULT,taglist);
      menu_ctx  = IUtility->GetTagData(IMTAG_MenuContext,(uint32)NULL,taglist);
 
      /* Finally, get the address of the menu which originated the event and
       * the ID number that uniquely identifies it (if it's a context menu).
       */
      menustrip = imsg->IAddress;
      IIntuition->GetAttr(MA_ID,menustrip,&menu_id);
 
      if (menu_type == IMT_CONTEXT_WINDOW)
      {
        /* Handle window context menus here */
 
        if (menu_id == CMENU_DOCKICON)
        {
          /* Our window context menu hook uses the Context
           * field to store the address of the object the
           * context menu is brought up for. This allows to
           * reuse the same context menu for all objects of
           * the same type.
           */
          dockicon = (struct DockIcon *)menu_ctx;
 
          while (longcode != NO_MENU_ID)
          {
            switch (longcode)
            {
              case MID_ICON_EDIT:
                EditDockIcon(dockicon);
                break;
 
              case MID_ICON_REVEAL:
                RevealDockIcon(dockicon);
                break;
 
              ...
            }
 
            longcode = IIntuition->IDoMethod(menustrip,MM_NEXTSELECT,0,longcode);
          }
        }
        else if (menu_id == CMENU_DOCK)
        {
          dock = (struct Dock *)menu_ctx;
 
          while (longcode != NO_MENU_ID)
          {
            ...
          }
        }
        else if (menu_id == CMENU_DOCKY)
        {
          docky = (struct Docky *)menu_ctx;
          ...
        }
      }
      else if (menu_type == IMT_CONTEXT_GADGET_APP)
      {
        /* Handle gadget context menus owned by the application here */
 
        if (menu_id == CMENU_GETCOLORGADGET)
        {
          while (longcode != NO_MENU_ID)
          {
            switch (longcode)
            {
              case MID_GETCOLOR_RESET_TO_DEFAULTS:
                ResetGetColorGadget((struct Gadget *)menu_ctx);
                break;
 
              ...
            }
 
            longcode = IIntuition->IDoMethod(menustrip,MM_NEXTSELECT,0,longcode);
          }
        }
        else if (menu_id == ...)
        {
          ...
        }
      }
      else if (menu_type == IMT_CONTEXT_GADGET_OBJ)
      {
        /* Handle gadget context menus spawned by gadgets here */
 
        IIntuition->IDoMethod((Object *)menu_ctx,GM_MENUPICK,NULL,menustrip,longcode,win);
      }
      else  /* menu_type == IMT_DEFAULT */
      {
        /* Handle normal window menus here */
 
        while (longcode != NO_MENU_ID)
        {
          switch (longcode)
          {
            case MID_EDITPREFS:
              EditPreferences();
              break;
 
            case MID_OPENPREFS:
              OpenPreferences();
              break;
 
            ...
          }
 
          longcode = IIntuition->IDoMethod(menustrip,MM_NEXTSELECT,0,longcode);
        }
      }
 
      break;
 
      ...
  }

Context menus and window.class

When using window.class there is no direct access to the IntuiMessage unless an IDCMP hook is used. To keep menu handling a little simpler, window.class (V54+) provides a few attributes that can be read upon WMHI_MENUPICK/WMHI_MENUHELP and carry the same extra information found in menu-related (Ext)IntuiMessages.

These attributes are:

WINDOW_MenuType
the data from IMTAG_MenuType
WINDOW_MenuContext
the data from IMTAG_MenuContext
WINDOW_MenuAddress
the value of the IntuiMessage's IAddress field

You can query these attributes of your window object when you need to handle a menu event that might come from a context menu. Note that if your application doesn't use context menus, you never need to care about these attributes.

Also note that when using window.class you'll never see IMT_CONTEXT_GADGET_OBJ events, as these are handled transparently by the class.

Peculiarities of context menus compared to normal menus

It is important to point out that, contrary to normal menus, a context menu can be brought up by a RMB click even when its window is inactive, as long as the mouse pointer is located over the GUI element it is tied to. This allows to add context menus to icons of docks, or gadgets of floating toolbars, and have them always open instantly as the user would expect, without any need for him/her to manually activate the respective window first.

If this behavior is not needed or desired, just check if the relevant window is active in your window's context menu hook or in your gadget's GM_QUERY method dispatcher, and return a NULL context menu pointer if that is not the case.

Another significant difference between context menus and normal ones is that a context menu doesn't currently support keyboard shortcuts, as the same context menu can be tied to more than one GUI element, and no way is defined to decide which one of those element should hear the key press. (Intuition still displays the shortcut if you add it to an item, but it will be non-functional.)

As of V54, context menus do not support the "menu verify" mechanism and always open immediately. This may or may not change in future Intuition versions.

Note
Context menus are always non-blocking.