Copyright (c) Hyperion Entertainment and contributors.

Difference between revisions of "Commodities Exchange Library"

From AmigaOS Documentation Wiki
Jump to navigation Jump to search
(Replaced the popshell.c example source code by a version adapted for OS4 by xenic.)
(Replaced BYTE, ULONG and LONG with int8, uint32 and int32, respectively.)
Line 1,238: Line 1,238:
 
CONST_STRPTR newshell = "\rllehswen"; /* "newshell" spelled backwards */
 
CONST_STRPTR newshell = "\rllehswen"; /* "newshell" spelled backwards */
 
struct InputEvent *ie = NULL;
 
struct InputEvent *ie = NULL;
ULONG cxsigflag;
+
uint32 cxsigflag;
   
 
#define TEMPLATE "HOTKEY/K,CX_PRIORITY/N"
 
#define TEMPLATE "HOTKEY/K,CX_PRIORITY/N"
Line 1,255: Line 1,255:
 
struct RDArgs *argsdata = NULL;
 
struct RDArgs *argsdata = NULL;
 
int32 rargs[ARG_MAX] = {0};
 
int32 rargs[ARG_MAX] = {0};
BYTE priority = 0;
+
int8 priority = 0;
   
 
signal(SIGINT, SIG_IGN);
 
signal(SIGINT, SIG_IGN);
Line 1,266: Line 1,266:
 
{
 
{
 
if (rargs[ARG_PRIORITY])
 
if (rargs[ARG_PRIORITY])
priority = (BYTE)*(uint32 *)rargs[ARG_PRIORITY];
+
priority = (int8)*(uint32 *)rargs[ARG_PRIORITY];
 
if ((broker_mp = IExec->AllocSysObject(ASOT_PORT, NULL)))
 
if ((broker_mp = IExec->AllocSysObject(ASOT_PORT, NULL)))
 
{
 
{
Line 1,332: Line 1,332:
 
extern struct MsgPort *broker_mp;
 
extern struct MsgPort *broker_mp;
 
extern CxObj *broker;
 
extern CxObj *broker;
extern ULONG cxsigflag;
+
extern uint32 cxsigflag;
 
CxMsg *msg = NULL;
 
CxMsg *msg = NULL;
ULONG sigrcvd, msgid, msgtype;
+
uint32 sigrcvd, msgid, msgtype;
LONG returnvalue = 1L;
+
int32 returnvalue = 1L;
   
 
while (returnvalue)
 
while (returnvalue)

Revision as of 11:06, 6 December 2015

Codereview.png Code samples on this page are not yet updated to AmigaOS 4.x some of them may be obsolete or incompatible with AmigaOS 4.x.

Commodities Exchange Library

This article describes Commodities Exchange, the library of routines used to add a custom input handler to the Amiga. With Commodities Exchange, any program function can be associated with key combinations or other input events globally allowing the creation utility programs that run in the background for all tasks.

Custom Input Handlers

The input.device has a hand in almost all user input on the Amiga. It gathers input events from the keyboard, the gameport (mouse), and several other sources, into one input "stream". Special programs called input event handlers intercept input events along this stream, examining and sometimes changing the input events. Both Intuition and the console device use input handlers to process user input.

The Amiga Input Stream

Using the input.device, a program can introduce its own custom handler into the chain of input handlers at almost any point in the chain. "Hot key" programs, shell pop-up programs, and screen blankers all commonly use custom input handlers to monitor user input before it gets to the Intuition input handler.

A Custom Input Handler

Custom input handlers do have their drawbacks, however. Not only are these handlers hard to program, but because there is no standard way to implement and control them, multiple handlers often do not work well together. Their antisocial behavior can result in load order dependencies and incompatibilities between different custom input handlers. Even for the expert user, having several custom input handlers coexist peacefully can be next to impossible.

The Commodities Network

Commodities Exchange eliminates these problems by providing a simple, standardized way to program and control custom input handlers. It is divided into two parts: an Exec library and a controller program.

The Exec library is called commodities.library. When it is first opened, commodities.library establishes a single input handler just before Intuition in the input chain. When this input handler receives an input event, it creates a CxMessage (Commodities Exchange Message) corresponding to the input event, and diverts the CxMessage through the network of Commodities Exchange input handlers.

These handlers are made up of trees of different CxObjects (Commodities Exchange Objects), each of which performs a simple operation on the CxMessages. Any CxMessages that exit the network are returned to the input.device's input stream as input events.

Through function calls to the commodities.library, an application can install a custom input handler. A Commodities Exchange application, sometimes simply referred to as a commodity, uses the CxObject primitives to do things such as filter certain CxMessages, translate CxMessages, signal a task when a CxObject receives a CxMessage, send a message when a CxObject receives a CxMessage, or if necessary, call a custom function when a CxObject receives a CxMessage.

Commodities Control

The standard controller program is called Exchange. It is located in the Utilities/Commodities drawer on your system partition. With Exchange, the user can monitor and control all the currently running Commodities Exchange applications from this one program. The user can enable and disable a commodity, kill a commodity, or, if the commodity has a window, ask the commodity to show or hide its window. When the user requests any of these actions, the controller program sends the commodity a message, telling it which action to perform.

Starting with version 53.4 of the commodities.library, functions are available that allow developers to write their own commodity control programs (Exchange replacements). These functions were not public previously.

Important Notes

  • Commodities are special-purpose programs. In fact, most programs or applications are not suitable for becoming commodities. The Commodities Exchange framework is ideal for programs that need to monitor all user input: hotkey utilities, screen blankers, mouse blankers, etc.
  • In the past, some developers turned their programs into commodities only to provide them with a handy keyboard shortcut to bring up/close their GUI. Please note that such practice is actually a misuse of the commodities framework.
  • Commodities Exchange should never be used as an alternate method of receiving user input for an application. Other applications depend on getting user input in some form or another from the input stream. A greedy program that diverts input to itself rather than letting the input go to where the user expects it can seriously confuse the user, not to mention compromise the advantages of multitasking.

CxObjects

CxObjects are the basic building blocks used to construct a commodity. A commodity uses CxObjects to take care of all manipulations of CxMessages. When a CxMessage "arrives" at a CxObject, that CxObject carries out its primitive action and then, if it has not deleted the CxMessage, it passes the CxMessage on to the next CxObject. A commodity links together CxObjects into a tree, organizing these simple action objects to perform some higher function.

A CxObject is in one of two states, active or inactive. An active CxObject performs its primitive action every time it receives a CxMessage. If a CxObject is inactive, CxMessages bypass it, continuing to the CxObject that follows the inactive one. By default, all CxObjects except the type called brokers are created in the active state.

Currently, there are seven types of CxObjects:

Commodities Exchange Object Types
Object Type Purpose
Broker Registers a new commodity with the commodity network
Filter Accepts or rejects input events based on criteria set up by the application
Sender Sends a message to a message port
Translate Replaces the input event with a different one
Signal Signals a task
Custom Calls a custom function provided by the commodity
Debug Sends debug information out the serial port

Installing A Broker Object

The Commodities Exchange input handler maintains a master list of CxObjects to which it diverts input events using CxMessages. The CxObjects in this master list are a special type of CxObject called brokers. The only thing a broker CxObject does is divert CxMessages to its own personal list of CxObjects. A commodity creates a broker and attaches other CxObjects to it. These attached objects take care of the actual input handler related work of the commodity and make up the broker's personal list.

The first program listing, "Broker.c", is a very simple example of a working commodity. It serves only to illustrate the basics of a commodity, not to actually perform any useful function. It shows how to set up a broker and process commands from the controller program.

Besides opening commodities.library and creating an Exec message port, setting up a commodity requires creating a broker. The function CxBroker() creates a broker and adds it to the master list.

CxObj *CxBroker(struct NewBroker *nb, LONG *error);

CxBroker()'s first argument is a pointer to a NewBroker structure:

struct NewBroker {
   BYTE     nb_Version;   /* There is an implicit pad byte after this BYTE */
   BYTE     *nb_Name;
   BYTE     *nb_Title;
   BYTE     *nb_Descr;
   SHORT    nb_Unique;
   SHORT    nb_Flags;
   BYTE     nb_Pri;       /* There is an implicit pad byte after this BYTE */
   struct   MsgPort   *nb_Port;
   WORD     nb_ReservedChannel;  /* Unused, make zero for future compatibility */
};

Commodities Exchange gets all the information it needs about the broker from this structure. NewBroker's nb_Version field contains the version number of the NewBroker structure. This should be set to NB_VERSION which is defined in <libraries/commodities.h>. The nb_Name, nb_Title, and nb_Descr point to strings which hold the name, title, and description of the broker. The two bit fields, nb_Unique and nb_Flags, toggle certain features of Commodities Exchange based on their values. They are discussed in detail later in this article.

The nb_Pri field contains the broker's priority. Commodities Exchange inserts the broker into the master list based on this number. Higher priority brokers get CxMessages before lower priority brokers.

CxBroker()'s second argument is a pointer to a LONG. If this pointer is not NULL, CxBroker() fills in this field with one of the following error return codes from <libraries/commodities.h>:

CBERR_OK        0        /* No error                         */
CBERR_SYSERR    1        /* System error , no memory, etc    */
CBERR_DUP       2        /* uniqueness violation             */
CBERR_VERSION   3        /* didn't understand nb_VERSION     */

Once the broker object is created with CxBroker(), it must be activated with ActivateCxObject().

LONG oldactivationvalue = ActivateCxObj(CxObj *co, LONG newactivationvalue);

After successfully completing the initial set up and activating the broker, a commodity can begin its input processing loop waiting for CxMessages to arrive.

CxMessages

There are actually two types of CxMessages. The first, CXM_IEVENT, corresponds to an input event and travels through the Commodities Exchange network. The other type, CXM_COMMAND, carries a command to a commodity. A CXM_COMMAND normally comes from the controller program and is used to pass user commands on to a commodity. A commodity receives these commands through an Exec message port that the commodity sets up before it calls CxBroker(). The NewBroker's nb_Port field points to this message port. A commodity can tell the difference between the two types of CxMessages by calling the CxMsgType() function.

ULONG  CxMsgType( CxMsg *cxm );
UBYTE *CxMsgData( CxMsg *cxm );
LONG   CxMsgID  ( CxMsg *cxm );

A CxMessage not only has a type, it can also have a data pointer as well as an ID associated with it. The data associated with a CXM_IEVENT CxMessage is an InputEvent structure. By using the CxMsgData() function, a commodity can obtain a pointer to the corresponding InputEvent of a CXM_IEVENT message. Commodities Exchange gives an ID of zero to any CXM_IEVENT CxMessage that it introduces to the Commodities network but certain CxObjects can assign an ID to them.

For a CXM_COMMAND CxMessages, the data pointer is generally not used but the ID specifies a command passed to the commodity from the user operating the controller program. The CxMsgID() macro extracts the ID from a CxMessage.

A Simple Commodity Example

The example below, "Broker.c", receives input from one source, the controller program. The controller program sends a CxMessage each time the user clicks its Enable, Disable, or Kill gadgets. Using the CxMsgID() function, the commodity finds out what the command is and executes it.

// broker.c - Simple skeletal example of opening a broker
 
#include <exec/libraries.h>
#include <libraries/commodities.h>
#include <dos/dos.h>
 
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/commodities.h>
 
void ProcessMsg(void);
 
struct CommoditiesIFace *ICommodities = NULL;
 
CxObj *broker;
struct MsgPort *broker_mp;
ULONG cxsigflag;
 
struct NewBroker newbroker = {
    NB_VERSION,   /* nb_Version - Version of the NewBroker structure */
    "Amiga broker", /* nb_Name - Name Commodities uses to identify this commodity */
    "Broker",     /* nb_Title - Title of commodity that appears in CXExchange */
    "A simple example of a broker",  /* nb_Descr - Description of the commodity */
    0,            /* nb_Unique - Tells CX not to launch another commodity with same name */
    0,            /* nb_Flags - Tells CX if this commodity has a window */
    0,            /* nb_Pri - This commodity's priority */
    0,            /* nb_Port - MsgPort CX talks to */
    0             /* nb_ReservedChannel - reserved for later use */
};
 
int main()
{
    CxMsg *msg;
 
    struct Library *CxBase = = IExec->OpenLibrary("commodities.library", 50);
    ICommodities = (struct CommoditiesIFace*)IExec->GetInterface(CxBase, "main", 1, NULL);
    if (ICommodities != NULL)
    {
        /* Commodities talks to a Commodities application through */
        /* an Exec Message port, which the application provides   */
        if (broker_mp = IExec->AllocSysObjectTags(ASOT_PORT, TAG_END))
        {
            newbroker.nb_Port = broker_mp;
 
            /* The commodities.library function CxBroker() adds a borker to the
             * master list.  It takes two arguments, a pointer to a NewBroker
             * structure and a pointer to a LONG.  The NewBroker structure contains
             * information to set up the broker.  If the second argument is not
             * NULL, CxBroker will fill it in with an error code.
             */
            if (broker = ICommodities->CxBroker(&newbroker, NULL))
            {
                cxsigflag = 1L << broker_mp->mp_SigBit;
 
                /* After it's set up correctly, the broker has to be activated */
                ICommodities->ActivateCxObj(broker, 1L);
 
                /* the main processing loop */
                ProcessMsg();
 
                /* It's time to clean up.  Start by removing the broker from the
                 * Commodities master list.  The DeleteCxObjAll() function will
                 * take care of removing a CxObject and all those connected
                 * to it from the Commodities network
                 */
                ICommodities->DeleteCxObj(broker);
 
                /* Empty the port of CxMsgs */
                while(msg = (CxMsg *)IExec->GetMsg(broker_mp))
                        IExec->ReplyMsg((struct Message *)msg);
            }
            IExec->FreeSysObject(ASOT_PORT, broker_mp);
        }
    }
 
    IExec->DropInterface((struct Interface*)ICommodities);
    IExec->CloseLibrary(CxBase);
 
    return 0;
}
 
 
void ProcessMsg(void)
{
    CxMsg *msg;
 
    uint32 sigrcvd, msgid, msgtype;
    int32 returnvalue = 1;
 
    while (returnvalue)
    {
        /* wait for something to happen */
        sigrcvd = IExec->Wait(SIGBREAKF_CTRL_C | cxsigflag);
 
        /* process any messages */
        while(msg = (CxMsg *)IExec->GetMsg(broker_mp))
        {
            /* Extract necessary information from the CxMessage and return it */
            msgid = ICommodities->CxMsgID(msg);
            msgtype = ICommodities->CxMsgType(msg);
            IExec->ReplyMsg((struct Message *)msg);
 
            switch(msgtype)
            {
                case CXM_IEVENT:
                    /* Shouldn't get any of these in this example */
                    break;
                case CXM_COMMAND:
                    /* Commodities has sent a command */
                    printf("A command: ");
                    switch(msgid)
                    {
                        case CXCMD_DISABLE:
                            IDOS->Printf("CXCMD_DISABLE\n");
                            /* The user clicked Commodities Exchange disable
                             * gadget better disable
                             */
                            ICommodities->ActivateCxObj(broker, 0);
                            break;
                        case CXCMD_ENABLE:
                            /* user clicked enable gadget */
                            IDOS->Printf("CXCMD_ENABLE\n");
                            ICommodities->ActivateCxObj(broker, 1);
                            break;
                        case CXCMD_KILL:
                            /* user clicked kill gadget, better quit */
                            IDOS->Printf("CXCMD_KILL\n");
                            returnvalue = 0;
                            break;
                    }
                    break;
                default:
                    IDOS->Printf("Unknown msgtype\n");
                    break;
            }
        }
        /* Test to see if user tried to break */
        if (sigrcvd & SIGBREAKF_CTRL_C)
        {
            returnvalue = 0;
            IDOS->Printf("CTRL C signal break\n");
        }
    }
}

Notice that "Broker.c" uses Ctrl-C as a break key. The break key for any commodity should be Ctrl-C.

Controller Commands

The commands that a commodity can receive from the controller program (as defined in <libraries/commodities.h>) are:

CXCMD_DISABLE     /* please disable yourself       */
CXCMD_ENABLE      /* please enable yourself        */
CXCMD_KILL        /* go away for good              */
CXCMD_APPEAR      /* open your window, if you can  */
CXCMD_DISAPPEAR   /* hide your window              */

The CXCMD_DISABLE, CXCMD_ENABLE, and CXCMD_KILL commands correspond to the similarly named controller program gadgets, Disable, Enable, and Kill; CXCMD_APPEAR and CXCMD_DISAPPEAR correspond to the controller program gadgets, Show and Hide. These gadgets are ghosted in Broker.c because it has no window (It doesn't make much sense to give the user a chance to click the Show and Hide gadgets). In order to do this, Broker.c has to tell Commodities Exchange to ghost these gadgets. When CxBroker() sets up a broker, it looks at the NewBroker.nb_Flags field to see if the COF_SHOW_HIDE bit (from <libraries/commodities.h>) is set. If it is, the "Show" and "Hide" gadgets for this broker will be selectable. Otherwise they are ghosted and disabled.

Shutting Down the Commodity

Shutting down a commodity is easy. After replying to all CxMessages waiting at the broker's message port, a commodity can delete its CxObjects. The DeleteCxObj() function removes a single CxObject from the Commodities network. DeleteCxObjectAll() removes multiple objects.

VOID DeleteCxObj( CxObj *co );
VOID DeleteCxObjAll( CxObj *delete_co );

If a commodity has a lot of CxObjects, deleting each individually can be a bit tedious. DeleteCxObjAll() will delete a CxObject and any other CxObjects that are attached to it. The HotKey.c example given later in this article uses this function to delete all its CxObjects. A commodity that uses DeleteCxObjAll() to delete all its CxObjects should make sure that they are all connected to the main one. (See the "Connecting CxObjects" section below.)

After deleting its CxObjects, a commodity must take care of any CxMessages that might have arrived at the message port just before the commodity deleted its objects.

while(msg = (CxMsg *)IExec->GetMsg(broker_mp))
    IExec->ReplyMsg((struct Message *)msg);

Commodity Tool Types

A goal of Commodities Exchange is to improve user control over input handlers. One way in which it accomplishes this goal is through the use of standard icon Tool Types. The user will expect commodities to recognize the set of standard Tool Types:

  • CX_PRIORITY
  • CX_POPUP
  • CX_POPKEY

CX_PRIORITY lets the user set the priority of a commodity. The string "CX_PRIORITY=" is a number from -128 to 127. The higher the number, the higher the priority of the commodity, giving it access to input events before lower priority commodities. All commodities should recognize CX_PRIORITY.

CX_POPUP and CX_POPKEY are only relevant to commodities with a window. The string "CX_POPUP=" should be followed by a "yes" or "no", telling the commodity if it should or shouldn't show its window when it is first launched. CX_POPKEY is followed by a string describing the key to use as a hot key for making the commodity's window appear (pop up). The description string for CX_POPKEY describes an input event. The specific format of the string is discussed in the next section ("Filter Objects and the Input Description String").

Obsolete Parsing Functions

Prior to AmigaOS 4.0, the Commodities Library had the following support functions which were included in an external link library:

UBYTE **ArgArrayInit(LONG argc, UBYTE **argv);
VOID ArgArrayDone(void);
STRPTR ArgString(UBYTE **tooltypearray, STRPTR tooltype, STRPTR defaultvalue);
LONG *ArgInt(UBYTE **tooltypearray, STRPTR tooltype, LONG defaultvalue);

These functions are no longer available. Use IDOS->ReadArgs(), IIcon->FindToolType() and IIcon->MatchToolValue() instead, depending on whether the commodity was launched from Workbench or the Shell (CLI).

Filter Objects and Input Description Strings

Because not all commodities are interested in every input event that makes it way down the input chain, Commodities Exchange has a method for filtering them. A filter CxObject compares the CxMessages it receives to a pattern. If a CxMessage matches the pattern, the filter diverts the CxMessage down its personal list of CxObjects.

CxObj *CxFilter(STRPTR descriptionstring);

The C macro CxFilter() (defined in <libraries/commodities.h>) returns a pointer to a filter CxObject. The macro has only one argument, a pointer to a string describing which input events to filter. The following regular expression outlines the format of the input event description string (CX_POPKEY uses the same description string format):

[class]  { [-] (qualifier | synonym) ) }  [ [-] upstroke]  [highmap | ANSICode]

Class can be any one of the class strings in the table below. Each class string corresponds to a class of input event as defined in <devices/inputevent.h>. Commodities Exchange will assume the class is rawkey if the class is not explicitly stated.

Class String Input Event Class
"rawkey" IECLASS_RAWKEY
"rawmouse" IECLASS_RAWMOUSE
"event" IECLASS_EVENT
"pointerpos" IECLASS_POINTERPOS
"timer" IECLASS_TIMER
"newprefs" IECLASS_NEWPREFS
"diskremoved" IECLASS_DISKREMOVED
"diskinserted" IECLASS_DISKINSERTED

Qualifier is one of the qualifier strings from the table below. Each string corresponds to an input event qualifier as defined in <devices/inputevent.h>). A dash preceding the qualifier string tells the filter object not to care if that qualifier is present in the input event. Notice that there can be more than one qualifier (or none at all) in the input description string.

Qualifier String Input Event Class
"lshift" IEQUALIFIER_LSHIFT
"rshift" IEQUALIFIER_RSHIFT
"capslock" IEQUALIFIER_CAPSLOCK
"control" IEQUALIFIER_CONTROL
"lalt" IEQUALIFIER_LALT
"ralt" IEQUALIFIER_RALT
"lcommand" IEQUALIFIER_LCOMMAND
"rcommand" IEQUALIFIER_RCOMMAND
"numericpad" IEQUALIFIER_NUMERICPAD
"repeat" IEQUALIFIER_REPEAT
"midbutton" IEQUALIFIER_MIDBUTTON
"rbutton" IEQUALIFIER_RBUTTON
"leftbutton" IEQUALIFIER_LEFTBUTTON
"relativemouse" IEQUALIFIER_RELATIVEMOUSE

Synonym is one of the synonym strings from the table below. These strings act as synonyms for groups of qualifiers. Each string corresponds to a synonym identifier as defined in <libraries/commodities.h>. A dash preceding the synonym string tells the filter object not to care if that synonym is present in the input event. Notice that there can be more than one synonym (or none at all) in the input description string.

Synonym String Synonym Identifier Description
"shift" IXSYM_SHIFT Look for either Shift key
"caps" IXSYM_CAPS Look for either Shift key or Caps Lock
"alt" IXSYM_ALT Look for either Alt key

Upstroke is the literal string "upstroke". If this string is absent, the filter considers only downstrokes. If it is present alone, the filter considers only upstrokes. If preceded by a dash, the filter considers both upstrokes and downstrokes.

Highmap is one of the following strings:

"backspace", "del", "down", "enter", "esc", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11, "f12", "help", "left", "return", "right", "space", "tab", "up".

ANSICode is a single character (for example "a" that Commodities Exchange looks up in the system default keymap.

Here are some example description strings. For function key F2 with the left Shift and either Alt key pressed, the input description string would be:

"rawkey lshift alt f2"

To specify the key that produces an "a" (this may or may not be the A key depending on the keymap), with or without any Shift, Alt, or Control keys pressed use:

"-shift -alt -control a"

For a mouse move with the right mouse button down, use:

"rawmouse rbutton"

To specify a timer event use:

"timer"

Connecting CxObjects

A CxObject has to be inserted into the Commodities network before it can process any CxMessages. AttachCxObj() adds a CxObject to the personal list of another CxObject. The HotKey.c example uses it to attach its filter to a broker.

VOID AttachCxObj ( CxObj *headobj, CxObj *co);
VOID InsertCxObj ( CxObj *headobj, CxObj *co, CxObj *co_pred );
VOID EnqueueCxObj( CxObj *headobj, CxObj *co );
VOID SetCxObjPri ( CxObj *co, LONG pri );
VOID RemoveCxObj ( CxObj *co );

AttachCxObj() adds the CxObject to the end of headobj's personal list. The ordering of a CxObject list determines which object gets CxMessages first. InsertCxObj() also inserts a CxObject, but it inserts it after another CxObject already in the personal list (co_pred in the prototype above).

Brokers aren't the only CxObjects with a priority. All CxObjects have a priority associated with them. To change the priority of any CxObject, use the SetCxObjPri() function. A commodity can use the priority to keep CxObjects in a personal list sorted by their priority. The commodities.library function EnqueueCxObj() inserts a CxObject into another CxObject's personal list based on priority.

Like its name implies, the RemoveCxObj() function removes a CxObject from a personal list. Note that it is not necessary to remove a CxObject from a list in order to delete it.

/* HotKey.c - Simple hot key commodity
 */
#include <exec/libraries.h>
#include <libraries/commodities.h>
#include <dos/dos.h>
 
#include <proto/exec.h>
#include <proto/commodities.h>
#include <proto/icon.h>
 
#define EVT_HOTKEY 1L
 
void ProcessMsg(void);
 
struct CommoditiesIFace *ICommodities;
struct IconIFace *IIcon;
 
struct MsgPort *broker_mp;
CxObj *broker, *filter, *sender, *translate;
 
struct NewBroker newbroker = {
    NB_VERSION,
    "Amiga HotKey",           /* string to identify this broker */
    "A Simple HotKey",
    "A simple hot key commodity",
    NBU_UNIQUE | NBU_NOTIFY,    /* Don't want any new commodities starting with this name. */
    0, 0, 0, 0                  /* If someone tries it, let me know */
};
 
uint32 cxsigflag;
 
int main(int argc, char **argv)
{
    uint8 *hotkey, **ttypes;
    CxMsg *msg;
 
    struct Library *CxBase = IExec->OpenLibrary("commodities.library", 50);
    ICommodities = (struct CommoditiesIFace*)IExec->GetInterface(CxBase, "main", 1, NULL);
 
    /* open the icon.library for the support library */
    /* functions, ArgArrayInit() and ArgArrayDone()  */
    struct Library *IconBase = IExec->OpenLibrary("icon.library", 50);
    struct IconIFace *IIcon = (struct IconIFace*)IExec->GetInterface(IconBase, "main", 1, NULL);
 
    if (ICommodities != NULL && IIcon != NULL)
    {
            if (broker_mp = CreateMsgPort())
            {
                newbroker.nb_Port = broker_mp;
                cxsigflag = 1L << broker_mp->mp_SigBit;
 
                /* ArgArrayInit() is a support library function (from the 2.0 version
                 * of amiga.lib) that makes it easy to read arguments from either a
                 * CLI or from Workbench's ToolTypes.  Because it uses icon.library,
                 * the library has to be open before calling this function.
                 * ArgArrayDone() cleans up after this function.
                 */
                ttypes = ArgArrayInit(argc, argv);
 
                /* ArgInt() (also from amiga.lib) searches through the array set up
                 * by ArgArrayInit() for a specific ToolType.  If it finds one, it
                 * returns the numeric value of the number that followed the
                 * ToolType (i.e., CX_PRIORITY=7). If it doesn't find the ToolType,
                 * it returns the default value (the third argument)
                 */
                newbroker.nb_Pri = (int8)ArgInt(ttypes, "CX_PRIORITY", 0);
 
                /* ArgString() works just like ArgInt(), except it returns a pointer to a string
                 * rather than an integer. In the example below, if there is no ToolType
                 * "HOTKEY", the function returns a pointer to "rawkey control esc".
                 */
                hotkey = ArgString(ttypes, "HOTKEY", "rawkey control esc");
 
                if (broker = ICommodities->CxBroker(&newbroker, NULL))
                {
                    /* CxFilter() is a macro that creates a filter CxObject.  This filter
                     * passes input events that match the string pointed to by hotkey.
                     */
                    if (filter = ICommodities->CxFilter(hotkey))
                    {
                        /* Add a CxObject to another's personal list */
                        ICommodities->AttachCxObj(broker, filter);
 
                        /* CxSender() creates a sender CxObject.  Every time a sender gets
                         * a CxMessage, it sends a new CxMessage to the port pointed to in
                         * the first argument. CxSender()'s second argument will be the ID
                         * of any CxMessages the sender sends to the port.  The data pointer
                         * associated with the CxMessage will point to a *COPY* of the
                         * InputEvent structure associated with the orginal CxMessage.
                         */
                        if (sender = CxSender(broker_mp, EVT_HOTKEY))
                        {
                            ICommodities->AttachCxObj(filter, sender);
 
                            /* CxTranslate() creates a translate CxObject. When a translate
                             * CxObject gets a CxMessage, it deletes the original CxMessage
                             * and adds a new input event to the input.device's input stream
                             * after the Commodities input handler. CxTranslate's argument
                             * points to an InputEvent structure from which to create the new
                             * input event.  In this example, the pointer is NULL, meaning no
                             * new event should be introduced, which causes any event that
                             * reaches this object to disappear from the input stream.
                             */
                            if (translate = ICommodities->CxTranslate(NULL))
                            {
                                ICommodities->AttachCxObj(filter, translate);
 
                                /* CxObjError() is a commodities.library function that returns
                                 * the internal accumulated error code of a CxObject.
                                 */
                                if (! CxObjError(filter))
                                {
                                    ICommodities->ActivateCxObj(broker, 1L);
                                    ProcessMsg();
                                }
                            }
                        }
                    }
                    /* DeleteCxObjAll() is a commodities.library function that not only
                     * deletes the CxObject pointed to in its argument, but it deletes
                     * all of the CxObjects that are attached to it.
                     */
                    ICommodities->DeleteCxObjAll(broker);
 
                    /* Empty the port of all CxMsgs */
                    while(msg = (CxMsg *)IExec->GetMsg(broker_mp))
                        IExec->ReplyMsg((struct Message *)msg);
                }
                DeletePort(broker_mp);
            }
            /* this amiga.lib function cleans up after ArgArrayInit() */
            ArgArrayDone();
    }
 
    IExec->DropInterface((struct Interface*)IIcon);
    IExec->CloseLibrary(IconBase);
 
    IExec->DropInterface((struct Interface*)ICommodities);
    IExec->CloseLibrary(CxBase);
 
    return 0;
}
 
void ProcessMsg(void)
{
    extern struct MsgPort *broker_mp;
    extern CxObj *broker;
    extern ULONG cxsigflag;
    CxMsg *msg;
    uint32 sigrcvd, msgid, msgtype;
    int32 returnvalue = 1;
 
    while(returnvalue)
    {
        sigrcvd = IExec->Wait(SIGBREAKF_CTRL_C | cxsigflag);
 
        while(msg = (CxMsg *)IExec->GetMsg(broker_mp))
        {
            msgid = ICommodities->CxMsgID(msg);
            msgtype = ICommodities->CxMsgType(msg);
            IExec->ReplyMsg((struct Message *)msg);
 
            switch(msgtype)
            {
                case CXM_IEVENT:
                    IDOS->Printf("A CXM_EVENT, ");
                    switch(msgid)
                    {
                        case EVT_HOTKEY: /* We got the message from the sender CxObject */
                            IDOS->Printf("You hit the HotKey.\n");
                            break;
                        default:
                            IDOS->Printf("unknown.\n");
                            break;
                    }
                    break;
                case CXM_COMMAND:
                    IDOS->Printf("A command: ");
                    switch(msgid)
                    {
                        case CXCMD_DISABLE:
                            IDOS->Printf("CXCMD_DISABLE\n");
                            ICommodities->ActivateCxObj(broker, 0L);
                            break;
                        case CXCMD_ENABLE:
                            IDOS->Printf("CXCMD_ENABLE\n");
                            ICommodities->ActivateCxObj(broker, 1L);
                            break;
                        case CXCMD_KILL:
                            IDOS->Printf("CXCMD_KILL\n");
                            returnvalue = 0;
                            break;
                        case CXCMD_UNIQUE:
                        /* Commodities Exchange can be told not only to refuse to launch a
                         * commodity with a name already in use but also can notify the
                         * already running commodity that it happened. It does this by
                         * sending a CXM_COMMAND with the ID set to CXMCMD_UNIQUE.  If the
                         * user tries to run a windowless commodity that is already running,
                         * the user wants the commodity to shut down. */
                            IDOS->Printf("CXCMD_UNIQUE\n");
                            returnvalue = 0;
                            break;
                        default:
                            IDOS->Printf("Unknown msgid\n");
                            break;
                    }
                    break;
                default:
                    IDOS->Printf("Unknown msgtype\n");
                    break;
            }
        }
        if (sigrcvd & SIGBREAKF_CTRL_C)
        {
            returnvalue = 0;
            IDOS->Printf("CTRL C signal break\n");
        }
    }
}

Sender CxObjects

A filter CxObject by itself is not especially useful. It needs some other CxObjects attached to it. A commodity interested in knowing if a specific key was pressed uses a filter to detect and divert the corresponding CxMessage down the filter's personal list. The filter does this without letting the commodity know what happened. The sender CxObject can be attached to a filter to notify a commodity that it received a CxMessage. CxSender() is a macro that creates a sender CxObject.

senderCxObj = CxObj *CxSender(struct MsgPort *senderport, LONG cxmID);

CxSender() supplies the sender with an Exec message port and an ID. For every CxMessage a sender receives, it sends a new CxMessage to the Exec message port passed in CxSender(). Normally, the commodity creates this port. It is not unusual for a commodity's broker and sender(s) to share an Exec message port. The HotKey.c example does this to avoid creating unnecessary message ports. A sender uses the ID (cxmID) passed to CxSender() as the ID for all the CxMessages that the it transmits. A commodity uses the ID to monitor CxMessages from several senders at a single message port.

A sender does several things when it receives a CxMessage. First, it duplicates the CxMessage's corresponding input event and creates a new CxMessage. Then, it points the new CxMessage's data field to the copy of the input event and sets the new CxMessage's ID to the ID passed to CxSender(). Finally, it sends the new CxMessage to the port passed to CxSender(), asynchronously.

Because HotKey uses only one message port between its broker and sender object, it has to extract the CxMessage's type so it can tell if it is a CXM_IEVENT or a CXM_COMMAND. If HotKey gets a CXM_IEVENT, it compares the CxMessage's ID to the sender's ID, EVT_HOTKEY, to see which sender sent the CxMessage. Of course HotKey has only one sender, so it only checks for only one ID. If it had more senders, HotKey would check for the ID of each of the other senders as well.

Although HotKey doesn't use it, a CXM_IEVENT CxMessage contains a pointer to the copy of an input event. A commodity can extract this pointer (using CxMsgData()) if it needs to examine the input event copy. This pointer is only valid before the CxMessage reply. Note that it does not make any sense to modify the input event copy.

Senders are attached almost exclusively to CxObjects that filter out most input events (usually a filter CxObject). Because a sender sends a CxMessage for every single input event it gets, it should only get a select few input events. The AttachCxObj() function can add a CxObject to the end of a filter's (or some other filtering CxObject's) personal list. A commodity should not attach a CxObject to a sender as a sender ignores any CxObjects in its personal list.

Translate CxObjects

Normally, after a commodity processes a hot key input event, it needs to eliminate that input event. Other commodities may need to replace an input event with a different one. The translate CxObject can be used for these purposes.

translateCxObj = CxObj  *CxTranslate(struct InputEvent *newinputevent);

The macro CxTranslate() creates a new translate CxObject. CxTranslate()'s only argument is a pointer to a chain of one or more InputEvent structures.

When a translate CxObject receives a CxMessage, it eliminates the CxMessage and its corresponding input event from the system. The translator introduces a new input event, which Commodities Exchange copies from the InputEvent structure passed to CxTranslate() (newinputevent from the function prototype above), in place of the deleted input event.

A translator is normally attached to some kind of filtering CxObject. If it wasn't, it would translate all input events into the same exact input event. Like the sender CxObject, a translator does not divert CxMessages down its personal list, so it doesn't serve any purpose to add any to it.

VOID SetTranslate( CxObj *translator, struct InputEvent *ie );

It is possible to change the InputEvent structure that a translator looks at when it creates and introduces new input events into the input stream. The function SetTranslate() accepts a pointer to the new InputEvent structure, which the translator will duplicate and introduce when it receives a CxMessage.

HotKey utilizes a special kind of translator. Instead of supplying a new input event, HotKey passes a NULL to CxTranslate(). If a translator has a NULL new input event pointer, it does not introduce a new input event, but still eliminates any CxMessages and corresponding input events it receives.

CxObject Errors

A Commodities Exchange function that acts on a CxObject records errors in the CxObject's accumulated error field. The function CxObjError() returns a CxObject's error field.

int32 co_errorfield = CxObjError( CxObj *co );

Each bit in the error field corresponds to a specific type of error. The following is a list of the currently defined CxObject errors and their corresponding bit mask constants.

Error Constant Meaning
COERR_ISNULL CxObjError() was passed a NULL.
COERR_NULLATTACH Someone tried to attach a NULL CxObject to this CxObject.
COERR_BADFILTER This filter CxObject currently has an invalid filter description.
COERR_BADTYPE Someone tried to perform a type specific function on the wrong type of CxObject (for example calling SetFilter() on a sender CxObject).

The remaining bits are reserved for future use. HotKey.c checks the error field of its filter CxObject to make sure the filter is valid. HotKey.c does not need to check the other objects with CxObjError() because it already makes sure that these other objects are not NULL, which is the only other kind of error the other objects can cause in this situation.

Commodities Exchange has a function that clears a CxObject's accumulated error field, ClearCxObjError().

VOID ClearCxObjError( CxObj *co );

A commodity should be careful about using this, especially on a filter. If a commodity clears a filter's error field and the COERR_BADFILTER bit is set, Commodities Exchange will think that the filter is OK and start sending messages through it.

Uniqueness

When a commodity opens its broker, it can ask Commodities Exchange not to launch another broker with the same name (nb_Name). The purpose of the uniqueness feature is to prevent the user from starting duplicate commodities. If a commodity asks, Commodities Exchange will not only refuse to create a new, similarly named broker, but it will also notify the original commodity if someone tries to do so.

A commodity tells Commodities Exchange not to allow duplicates by setting certain bits in the nb_Unique field of the NewBroker structure it sends to CxBroker():

NBU_UNIQUE bit 0
NBU_NOTIFY bit 1

Setting the NBU_UNIQUE bit prevents duplicate commodities. Setting the NBU_NOTIFY bit tells Commodities Exchange to notify a commodity if an attempt was made to launch a duplicate. Such a commodity will receive a CXM_COMMAND CxMessage with an ID of CXCMD_UNIQUE when someone tries to duplicate it. Because the uniqueness feature uses the name a programmer gives a commodity to differentiate it from other commodities, it is possible for completely different commodities to share the same name, preventing the two from coexisting. For this reason, a commodity should not use a name that is likely to be in use by other commodities (like "filter" or "hotkey"). Instead, use a name that matches the commodity name.

When "HotKey.c" gets a CXCMD_UNIQUE CxMessage, it shuts itself down. "HotKey.c" and all the windowless commodities that come with Workbench shut themselves down when they get a CXCMD_UNIQUE CxMessage. Because the user will expect all windowless commodities to work this way, all windowless commodities should follow this standard.

When the user tries to launch a duplicate of a system commodity that has a window, the system commodity moves its window to the front of the display, as if the user had clicked the "Show" gadget in the controller program's window. A windowed commodity should mimic conventions set by existing windowed system commodities, and move its window to the front of the display.

Signal CxObjects

A commodity can use a sender CxObject to find out if a CxMessage has "visited" a CxObject, but this method unnecessarily uses system resources. A commodity that is only interested in knowing if such a visitation took place does not need to see a corresponding input event or a CxMessage ID. Instead, Commodities Exchange has a CxObject that uses an Exec signal.

CxObj *signalCxObj = CxSignal(struct Task *, LONG cx_signal);

CxSignal() sets up a signal CxObject. When a signal CxObject receives a CxMessage, it signals a task. The commodity is responsible for determining the proper task ID and allocating the signal. Normally, a commodity wants to be signaled so it uses FindTask(NULL) to find it's own task address. Note that cx_signal from the above prototype is the signal number as returned by AllocSignal(), not the signal mask made from that number. For more information on signals, see Exec Signals.

The example "Divert.c" (shown a little later) uses a signal CxObject.

Custom CxObjects

Although the CxObjects mentioned so far take care of most of the input event handling a commodity needs to do, they cannot do it all. This is why Commodities Exchange has a custom CxObject. When a custom CxObject receives a CxMessage, it calls a function provided by the commodity.

CxObject *customCxObj = CxCustom(int32 *customfunction(), int32 cxmID);

A custom CxObject is the only means by which a commodity can directly modify input events as they pass through the Commodities network as CxMessages. For this reason, it is probably the most dangerous of the CxObjects to use.

A Warning About Custom CxObjects
Unlike the rest of the code a commodities programmer writes, the code passed to a custom CxObject runs as part of the input.device task, putting severe restrictions on the function. No DOS or Intuition functions can be called. No assumptions can be made about the values of registers upon entry. Any function passed to CxCustom() should be very quick and very simple, with a minimum of stack usage.

Commodities Exchange calls a custom CxObject's function as follows:

VOID customfunction(CxMsg *cxm, CxObj *customcxobj);

where cxm is a pointer to a CxMessage corresponding to a real input event, and customcxobj is a pointer to the custom CxObject. The custom function can extract the pointer to the input event by calling CxMsgData(). Before passing the CxMessage to the custom function, Commodities Exchange sets the CxMessage's ID to the ID passed to CxCustom().

The following is an example of a custom CxObject function that swaps the function of the left and right mouse buttons.

custom = CxCustom(CxFunction, 0)
 
/* The custom function for the custom CxObject.  Any code for a custom CxObj must be */
/* short and sweet. This code runs as part of the input.device task */
#define CODEMASK (0x00FF & IECODE_LBUTTON & IECODE_RBUTTON)
 
void CxFunction(register CxMsg *cxm, CxObj *co)
{
    uint16 mousequals = 0x0000;
 
    /* Get the struct InputEvent associated with this CxMsg.  Unlike the InputEvent
     * extracted from a CxSender's CxMsg, this is a *REAL* input event, be careful with it.
     */
    struct InputEvent *ie = (struct InputEvent *)CxMsgData(cxm);
 
    /* Check to see if this input event is a left or right mouse button   */
    /* by itself (a mouse button can also be a qualifier).  If it is, flip the   */
    /* low order bit to switch leftbutton <--> rightbutton. */
    if (ie->ie_Class == IECLASS_RAWMOUSE)
        if ((ie->ie_Code & CODEMASK) == CODEMASK)  ie->ie_Code ^= 0x0001;
 
    /* Check the qualifiers. If a mouse button was down when this */
    /* input event occurred, set the other mouse button bit.      */
    if (ie->ie_Qualifier & IEQUALIFIER_RBUTTON)  mousequals |= IEQUALIFIER_LEFTBUTTON;
    if (ie->ie_Qualifier & IEQUALIFIER_LEFTBUTTON)  mousequals |= IEQUALIFIER_RBUTTON;
 
    /* clear the RBUTTON and LEFTBUTTON qualifier bits */
    ie->ie_Qualifier &= ~(IEQUALIFIER_LEFTBUTTON | IEQUALIFIER_RBUTTON);
 
    /* set the mouse button qualifier bits to their new values */
    ie->ie_Qualifier |= mousequals;
}

Debug CxObjects

The final CxObject is the debug CxObject. When a debug CxObject receives a CxMessage, it sends debugging information to the serial port using DebugPrintF().

CxObj *debugCxObj = CxDebug(int32 ID);

The debug CxObject will DebugPrintF() the following information about itself, the CxMsg, and the corresponding InputEvent structure:

DEBUG NODE: 7CB5AB0, ID: 2
 CxMsg: 7CA6EF2, type: 0, data 2007CA destination 6F1E07CB
dump IE: 7CA6F1E
 Class 1
 Code 40
 Qualifier 8000
 EventAddress 40001802

There has to be a terminal connected to the Amiga's serial port to receive this information.

The IX Structure

Commodities Exchange does not use the input event description strings discussed earlier to match input events. Instead, Commodities Exchange converts these strings to its own internal format. These input expressions are available for commodities to use instead of the input description strings. The following is the IX structure as defined in <libraries/commodities.h>:

#define IX_VERSION   2
 
struct InputXpression {
   UBYTE   ix_Version;     /* must be set to IX_VERSION  */
   UBYTE   ix_Class;       /* class must match exactly   */
   UWORD   ix_Code;
   UWORD   ix_CodeMask;    /* normally used for UPCODE   */
   UWORD   ix_Qualifier;
   UWORD   ix_QualMask;
   UWORD   ix_QualSame;    /* synonyms in qualifier      */
   };
 
typedef struct InputXpression IX;

The ix_Version field contains the current version number of the InputXpression structure. The current version is defined as IX_VERSION. The ix_Class field contains the IECLASS_ constant (defined in <devices/inputevent.h>) of the class of input event sought. Commodities Exchange uses the ix_Code and ix_CodeMask fields to match the ie_Code field of a struct InputEvent. The bits of ix_CodeMask indicate which bits are relevant in the ix_Code field when trying to match against a ie_Code. If any bits in ix_CodeMask are off, Commodities Exchange does not consider the corresponding bit in ie_Code when trying to match input events. This is used primarily to mask out the IECODE_UP_PREFIX bit of rawkey events, making it easier to match both up and down presses of a particular key.

IX's qualifier fields, ix_Qualifier, ix_QualMask, and ix_QualSame, are used to match the ie_Qualifier field of an InputEvent structure. The ix_Qualifier and ix_QualMask fields work just like ix_Code and ix_CodeMask. The bits of ix_QualMask indicate which bits are relevant when comparing ix_Qualifier to ie_Qualifier. The ix_QualSame field tells Commodities Exchange that certain qualifiers are equivalent:

#define IXSYM_SHIFT  1     /* left- and right- shift are equivalent     */
#define IXSYM_CAPS   2     /* either shift or caps lock are equivalent  */
#define IXSYM_ALT    4     /* left- and right- alt are equivalent       */

For example, the input description string

"rawkey -caps -lalt -relativemouse -upstroke ralt tab"

matches a tab upstroke or downstroke with the right Alt key pressed whether or not the left Alt, either Shift, or the Caps Lock keys are down. The following IX structure corresponds to that input description string:

IX ix = {
    IX_VERSION,                   /* The version */
    IECLASS_RAWKEY,               /* We're looking for a RAWKEY event */
    0x42,                         /* The key the usa0 keymap maps to a tab*/
    0x00FF & (~IECODE_UP_PREFIX), /* We want up and down key presses */
    IEQUALIFIER_RALT,             /* The right alt key must be down */
    0xFFFF & ~(IEQUALIFIER_LALT | IEQUALIFIER_LSHIFT |
        IEQUALIFIER_RSHIFT | IEQUALIFIER_CAPSLOCK | IEQUALIFIER_RELATIVEMOUSE),
        /* don't care about left alt, shift, capslock, or relativemouse qualifiers   */
    IXSYM_CAPS  /* The shift keys and the capslock key qualifiers are all equivalent */
};

The CxFilter() macro only accepts a description string to describe an input event. A commodity can change this filter, however, with the SetFilter() and SetFilterIX() function calls.

VOID SetFilter( CxObj *filter, STRPTR descrstring );
VOID SetFilterIX( CxObj *filter, IX *ix );

SetFilter() and SetFilterIX() change which input events a filter CxObject diverts. SetFilter() accepts a pointer to an input description string. SetFilterIX() accepts a pointer to an IX input expression. A commodity that uses either of these functions should check the filter's error code with CxObjError() to make sure the change worked.

The function ParseIX() parses an input description string and translates it into an IX input expression.

LONG errorcode = ParseIX( STRPTR descrstring, IX *ix );

Commodities Exchange uses ParseIX() to convert the description string in CxFilter() to an IX input expression. As was mentioned previously, ParseIX() does not work with certain kinds of input strings.

Controlling CxMessages

A Custom CxObject has the power to directly manipulate the CxMessages that travel around the Commodities network. One way is to directly change values in the corresponding input event. Another way is to redirect (or dispose of) the CxMessages.

VOID DivertCxMsg ( CxMsg *cxm, CxObj *headobj, CxObj *retobj );
VOID RouteCxMsg  ( CxMsg *cxm, CxObj *co );
VOID DisposeCxMsg( CxMsg *cxm );

DivertCxMsg() and RouteCxMsg() dictate where the CxMessage will go next. Conceptually, DivertCxMsg() is analogous to a subroutine in a program; the CxMessage will travel down the personal list of a CxObject (headobj in the prototype) until it gets to the end of that list. It then returns and visits the CxObject that follows the return CxObject (the return CxObject in the prototype above is retobj). RouteCxMsg() is analogous to a goto in a program; it has no CxObject to return to.

DisposeCxMsg() removes a CxMessage from the network and releases its resources. The translate CxObject uses this function to remove a CxMessage.

The example "Divert.c" shows how to use DivertCxMsg() as well as a signal CxObject.

;/* divert.c - commodity to monitor user inactivity - compiled with SASC 5.10
LC -b0 -cfist -v -j73 divert.c
Blink FROM LIB:c.o,divert.o TO divert LIBRARY LIB:LC.lib,LIB:Amiga.lib NODEBUG SC SD
quit; */
#include <exec/libraries.h>
#include <libraries/commodities.h>
#include <dos/dos.h>
#include <clib/exec_protos.h>
#include <clib/alib_protos.h>
#include <clib/alib_stdio_protos.h>
#include <clib/commodities_protos.h>
#include <devices/inputevent.h>

#ifdef LATTICE
int CXBRK(void) { return(0); }  /* Disable Lattice CTRL/C handling */
int chkabort(void) { return(0); }
#endif

#define TIMER_CLICKS 100

void main(int, char **);
void ProcessMsg(void);
void CxFunction(CxMsg *, CxObj *);

struct Library *CxBase, *IconBase;
struct MsgPort *broker_mp;
CxObj *broker, *cocustom, *cosignal;

struct NewBroker newbroker =
{
    NB_VERSION,
    "Divert",           /* string to identify this broker */
    "Divert",
    "show divert",
    NBU_UNIQUE | NBU_NOTIFY,  /* Don't want any new commodities starting with this name. */
    0, 0, 0, 0                /* If someone tries it, let me know                        */
};

struct Task *task;
ULONG cxsigflag, signal, cxobjsignal;

void main(int argc, char **argv)
{
    UBYTE **ttypes;
    CxMsg *msg;

    if (CxBase = OpenLibrary("commodities.library", 37L))
    {
        /* open the icon.library for support library functions, ArgArrayInit() and ArgArrayDone() */
        if (IconBase = OpenLibrary("icon.library", 36L))
        {
            if (broker_mp = CreateMsgPort())
            {
                newbroker.nb_Port = broker_mp;
                cxsigflag = 1L << broker_mp->mp_SigBit;

                /* ArgArrayInit() is a support library function (in the 2.0 version of amiga.lib) */
                /* that makes it easy to read arguments from either a CLI or from Workbench's     */
                /* ToolTypes. Because it uses icon.library, the library has to be open before     */
                /* before calling this function.  ArgArrayDone() cleans up after this function.   */
                ttypes = ArgArrayInit(argc, argv);

                /* ArgInt() (in amiga.lib) searches through the array set up by ArgArrayInit()    */
                /* for a specific ToolType.  If it finds one, it returns the numeric value of the */
                /* number that followed the ToolType (i.e., CX_PRIORITY=7).  If it  doesn't find  */
                /* the ToolType, it returns the default value (the third argument)                */
                newbroker.nb_Pri = (BYTE)ArgInt(ttypes, "CX_PRIORITY", 0);

                if (broker = CxBroker(&newbroker, NULL))
                {
                    /* CxCustom() takes two arguments, a pointer to the custom function           */
                    /* and an ID. Commodities Exchange will assign that ID to any CxMsg           */
                    /* passed to the custom  function.                                            */
                    if (cocustom = CxCustom(CxFunction, 0L))
                    {
                        AttachCxObj(broker, cocustom);

                        /* Allocate a signal bit for the signal CxObj */
                        if ( (signal = (ULONG)AllocSignal(-1L)) != -1)
                        {
                            /* set up the signal mask */
                            cxobjsignal = 1L << signal;
                            cxsigflag |= cxobjsignal;

                            /* CxSignal takes two arguments, a pointer to the task to signal      */
                            /* (normally the commodity) and the number of the signal bit the      */
                            /* commodity acquired to signal with.                                 */
                            task = FindTask(NULL);
                            if (cosignal = CxSignal(task, signal))
                            {
                                AttachCxObj(cocustom, cosignal);
                                ActivateCxObj(broker, 1L);
                                ProcessMsg();
                            }
                            FreeSignal(signal);
                        }
                    }
                    /* DeleteCxObjAll() is a commodities.library function that not only deletes   */
                    /* the CxObject pointed to in its argument, but it deletes all of the         */
                    /* CxObjects that are attached to it.                                         */
                    DeleteCxObjAll(broker);

                    /* Empty the port of all CxMsgs */
                    while(msg = (CxMsg *)GetMsg(broker_mp))
                        ReplyMsg((struct Message *)msg);
                }
                DeletePort(broker_mp);
            }
            ArgArrayDone();   /* this amiga.lib function cleans up after ArgArrayInit()           */
            CloseLibrary(IconBase);
        }
        CloseLibrary(CxBase);
    }
}

void ProcessMsg(void)
{
    extern struct MsgPort *broker_mp;
    extern CxObj *broker;
    extern ULONG cxsigflag;
    CxMsg *msg;
    ULONG sigrcvd, msgid;
    LONG returnvalue = 1L;

    while (returnvalue)
    {
        sigrcvd = Wait(SIGBREAKF_CTRL_C | cxsigflag);

        while(msg = (CxMsg *)GetMsg(broker_mp))
        {
            msgid = CxMsgID(msg);
            ReplyMsg((struct Message *)msg);

            switch(msgid)
            {
                case CXCMD_DISABLE:
                    ActivateCxObj(broker, 0L);
                    break;
                case CXCMD_ENABLE:
                    ActivateCxObj(broker, 1L);
                    break;
                case CXCMD_KILL:
                    returnvalue = 0L;
                    break;
                case CXCMD_UNIQUE:
                    returnvalue = 0L;
                    break;
            }
        }

        if (sigrcvd & SIGBREAKF_CTRL_C) returnvalue = 0L;

        /* Check to see if the signal CxObj signalled us. */
        if (sigrcvd & cxobjsignal) printf("Got Signal\n");
    }
}

/* The custom function for the custom CxObject.  Any code for a custom CxObj must be short        */
/* and sweet because it runs as part of the input.device task.                                    */
void CxFunction(register CxMsg *cxm, CxObj *co)
{
    struct InputEvent *ie;
    static ULONG time = 0L;

    /* Get the struct InputEvent associated with this CxMsg. Unlike the InputEvent                */
    /* extracted from a CxSender's CxMsg, this is a *REAL* input event, be careful with it.       */
    ie = (struct InputEvent *)CxMsgData(cxm);

    /* This custom function counts the number of timer events that go by while no other input     */
    /* events occur.  If it counts more than a certain amount of timer events, it clears the      */
    /* count and diverts the timer event CxMsg to the custom object's personal                    */
    /* list.  If an event besides a timer event passes by, the timer event count is reset.        */
    if (ie->ie_Class == IECLASS_TIMER)
    {
        time++;
        if (time >= TIMER_CLICKS)
        {
            time = 0L;
            DivertCxMsg(cxm, co, co);
        }
    }
    else
        time = 0L;
}

New Input Events

The Commodities Library also has functions used to introduce new input events to the input stream.

struct InputEvent *InvertString( UBYTE *string, ULONG *keymap );
VOID               FreeIEvents( struct InputEvent *ie );
VOID               AddIEvents( struct InputEvent *ie );

InvertString() is a function that accepts an ASCII string and creates a linked list of input events that translate into the string using the supplied keymap (or the system default if the key map is NULL). The NULL terminated string may contain ANSI character codes, an input description enclosed in angle (<>) brackets, or one of the following backslash escape characters:

\r Return
\t Tab
\\ Backslash

For example:

abc<alt f1>\rhi there.

FreeIEvents() frees a list of input events allocated by InvertString(). AddIEvents() is a commodities.library function that adds a linked list of input events at the the top of the Commodities network. Each input event in the list is made into an individual CxMessage. Note that if passed a linked list of input events created by InvertString(), the order the events appear in the string will be reversed.

Example

/* PopShell.c - Simple hot key commodity example.
   Adapted by xenic from the original source code.
 
   Compile commands: gcc PopShell.c -o popshell -lamiga -Wall
 
   To run the compiled program - Open 2 shell windows. Execute
   PopShell in the first shell window with a line like:
     PopShell HOTKEY "ctrl alt f"
   Activate the second shell window and enter the keyboard shortcut.
   A new shell window should open on the Workbench screen.
*/
 
#include <stdio.h>
#include <signal.h>
 
#include <libraries/commodities.h>
 
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/commodities.h>
#include <clib/alib_protos.h>
 
 
static CONST_STRPTR version USED = "$VER: PopShell 1.1 (05.11.2015)";
static CONST_STRPTR stackcookie USED = "$STACK: 32768";
 
#define EVT_HOTKEY 1L
 
struct Library *CxBase = NULL;
struct CommoditiesIFace *ICommodities = NULL;
 
struct MsgPort *broker_mp = NULL;
CxObj *broker, *filter;
 
struct NewBroker newbroker =
{
	NB_VERSION,
	"Amiga PopShell",       /* string to identify this broker */
	"A Simple PopShell",
	"A simple PopShell commodity",
	NBU_UNIQUE | NBU_NOTIFY,      /* Don't want any new commodities starting with this name. */
	0, 0, 0, 0                    /* If someone tries it, let me know */
};
 
CONST_STRPTR newshell = "\rllehswen";  /* "newshell" spelled backwards */
struct InputEvent *ie = NULL;
uint32 cxsigflag;
 
#define TEMPLATE "HOTKEY/K,CX_PRIORITY/N"
enum
{
	ARG_HOTKEY, ARG_PRIORITY, ARG_MAX
};
 
void ProcessMsg(void);
 
 
int main(int argc, char **argv)
{
	STRPTR hotkey = NULL;
	CxMsg *msg = NULL;
	struct RDArgs *argsdata = NULL;
	int32 rargs[ARG_MAX] = {0};
	int8  priority = 0;
 
	signal(SIGINT, SIG_IGN);
 
	if ((CxBase = IExec->OpenLibrary("commodities.library", 37L)))
	{
		if ((ICommodities = (struct CommoditiesIFace *)IExec->GetInterface(CxBase, "main", 1, NULL)))
		{
			if ((argsdata = IDOS->ReadArgs(TEMPLATE, rargs, NULL)))
			{
				if (rargs[ARG_PRIORITY])
					priority = (int8)*(uint32 *)rargs[ARG_PRIORITY];
				if ((broker_mp = IExec->AllocSysObject(ASOT_PORT, NULL)))
				{
					newbroker.nb_Port = broker_mp;
					cxsigflag = 1L << broker_mp->mp_SigBit;
					newbroker.nb_Pri = priority;
					hotkey = (STRPTR)rargs[ARG_HOTKEY];
 
					if ((broker = ICommodities->CxBroker(&newbroker, NULL)))
					{
						/* CxFilter(), CxSender() & CxTranslate() are    */
						/* macros defined in libraries/commodities.h.    */
						/* CxFilter() creates a filter CxObject.         */
						/* CxSender() creates a sender CxObject.         */
						/* CxTranslate() creates a translation CxObject. */
						if ((filter = CxFilter(hotkey)))
						{
							/* Add a filter CxObject to another's personal list. */
							ICommodities->AttachCxObj(broker, filter);
 
							/* Add a sender CxObject to the filter CxObject. */
							ICommodities->AttachCxObj(filter, CxSender(broker_mp, EVT_HOTKEY));
 
							/* Add a translation CxObject to the filter CxObject */
							ICommodities->AttachCxObj(filter, CxTranslate(NULL));
 
							if (!(ICommodities->CxObjError(filter)))
							{
								/* InvertString() is an amiga.lib function that creates a linked */
								/* list of input events which would translate into the string    */
								/* passed to it.  Note that it puts the input events in the      */
								/* opposite order in which the corresponding letters appear in   */
								/* the string.  A translate CxObject expects them backwards.     */
								if ((ie = InvertString(newshell, NULL)))
								{
									ICommodities->ActivateCxObj(broker, 1L);
									ProcessMsg();
									/* we have to release the memory allocated by InvertString. */
									FreeIEvents(ie);
								}
							}
						}
						/* DeleteCxObjAll() is a commodities.library function that */
						/* deletes the CxObject pointed to in its argument and     */
						/* deletes all of the CxObjects attached to it.            */
						ICommodities->DeleteCxObjAll(broker);
 
						/* Empty the port of all CxMsgs */
						while((msg = (CxMsg *)IExec->GetMsg(broker_mp)))
							IExec->ReplyMsg((struct Message *)msg);
					}
					IExec->FreeSysObject(ASOT_PORT, broker_mp);
				}
				IDOS->FreeArgs(argsdata); /* cleans up after ReadArgs() */
			}
			IExec->DropInterface((struct Interface *)ICommodities);
		}
		IExec->CloseLibrary(CxBase);
	}
	return RETURN_OK;
}
 
void ProcessMsg(void)
{
	extern struct MsgPort *broker_mp;
	extern CxObj *broker;
	extern uint32 cxsigflag;
	CxMsg *msg = NULL;
	uint32 sigrcvd, msgid, msgtype;
	int32  returnvalue = 1L;
 
	while (returnvalue)
	{
		sigrcvd = IExec->Wait(SIGBREAKF_CTRL_C | cxsigflag);
 
		while((msg = (CxMsg *)IExec->GetMsg(broker_mp)))
		{
			msgid = ICommodities->CxMsgID(msg);
			msgtype = ICommodities->CxMsgType(msg);
			IExec->ReplyMsg((struct Message *)msg);
 
			switch(msgtype)
			{
				case CXM_IEVENT:
					printf("A CXM_EVENT, ");
					switch(msgid)
					{
						case EVT_HOTKEY:
							/* We got the message from the sender CxObject  */
							printf("You hit the HotKey.\n");
							/* Add the string "newshell" to input * stream. */
							/*  If a shell gets it, it'll open a new shell. */
							ICommodities->AddIEvents(ie);
							break;
						default:
							printf("unknown.\n");
							break;
					}
					break;
				case CXM_COMMAND:
					printf("A command: ");
					switch(msgid)
					{
						case CXCMD_DISABLE:
						printf("CXCMD_DISABLE\n");
							ICommodities->ActivateCxObj(broker, 0L);
							break;
						case CXCMD_ENABLE:
							printf("CXCMD_ENABLE\n");
							ICommodities->ActivateCxObj(broker, 1L);
							break;
						case CXCMD_KILL:
							printf("CXCMD_KILL\n");
							returnvalue = 0L;
							break;
						case CXCMD_UNIQUE:
						/* Commodities Exchange can be told not only to refuse to launch a    */
						/* commodity with a name already in use but also can notify the       */
						/* already running commodity that it happened.  It does this by       */
						/* sending a CXM_COMMAND with the ID set to CXMCMD_UNIQUE. If the     */
						/* user tries to run a windowless commodity that is already running,  */
						/* the user wants the commodity to shut down.                         */
							printf("CXCMD_UNIQUE\n");
							returnvalue = 0L;
							break;
						default:
							printf("Unknown msgid\n");
							break;
					}
					break;
				default:
					printf("Unknown msgtype\n");
					break;
			}
		}
 
		if (sigrcvd & SIGBREAKF_CTRL_C)
		{
			returnvalue = 0L;
			printf("CTRL C signal break\n");
		}
	}
}

Function Reference

The following are brief descriptions of the Commodities Exchange functions covered in this article. See the SDK for details on each function call.

Function Description
CxBroker() Creates a CxObject of type Broker.
CxFilter() Creates a CxObject of type Filter.
CxSender() Creates a CxObject of type Sender.
CxTranslate() Creates a CxObject of type Translate.
CxSignal() Creates a CxObject of type Signal.
CxCustom() Creates a CxObject of type Custom.
CxDebug() Creates a CxObject of type Debug.
DeleteCxObj() Frees a single CxObject
DeleteCxObjAll() Frees a group of connected CxObjects
ActivateCxObj() Activates a newly created CxObject in the commodities network.
CxTranslate() Sets up substitution of one input event for another by translate CxObjects.
CxMsgType() Finds the type of a CxMessage.
CxMsgData() Returns the CxMessage data.
CxMsgID() Returns the CxMessage ID.
CxObjError() Returns the CxObject's accumulated error field.
ClearCxObjError() Clear the CxObject's accumulated error field.
AttachCxObj() Attaches a CxObject to the end of a given CxObject's list.
InsertCxObj() Inserts a CxObject in a given position in a CxObject's list.
EnqueueCxObj() Inserts a CxObject in a CxObject's list by priority.
SetCxObjPri() Sets a CxObject's priority for EnqueueCxObj().
RemoveCxObj() Removes a CxObject from a list.
SetFilter() Set a filter for a CxObject from an input description string.
SetFilterIX() Set a filter for a CxObject from an IX data structure.
ParseIX() Convert an input description string to an IX data structure.
DivertCxMsg() Divert a CxMessage to one CxObject and return it to another.
RouteCxMsg() Redirect a CxMessage to a new CxObject.
DisposeCxMsg() Cancel a CxMessage removing it from the Commodities network.
InvertString() Creates a linked list of input events that correspond to a given string.
FreeIEvents() Frees the linked list of input events created with InvertString().
AddIEvents() Converts a list of input events to CxMessages and puts them into the network.
CopyBrokerList() Creates a local copy of the current broker list (V53.4).
FreeBrokerList() Frees the local broker list (V53.4).
BrokerCommand() Sends a command to a commodity broker (V53.4).

Obsolete Functions

The following functions are obsolete and no longer used:

Function Description
ArgArrayInit() Create a Tool Types array from argc and argv (Workbench or Shell).
ArgArrayDone() Free the resources used by ArgArrayInit().
ArgString() Return the string associated with a given Tool Type in the array.
ArgInt() Return the integer associated with a given Tool Type in the array.