gem blog — programmers’ workbench

introduction

The GEM Programmers’ Workbench was originally written by me, Dylan Harris, as example code for some GEM programmers’ courses I ran back in the early ’80s, when I was young and spotty. It was written for Software Experts Ltd., a technical training company that has long since vanished. The code was distributed by Digital Research with the GEM programmers’ Toolkit in the UK, under an early public licence (similar to a BSD licence). It was done to advertise the services offered by Software Experts Ltd. to the target market (prospective GEM programmers). The code could be used for any purpose provided Software Experts was acknowledged—which I formally do here.

The Workbench is designed as a series of high level functions for accessing GEM. The idea is that they take away the necessity of producing functions to support some of GEM’s application requirements which will be common to many programs, such as window maintenance.

Initially, Caldera Inc. (now SCO), then Lineo Inc., the inheriters of Digital Research and the owners of GEM at the time, released the source code under an open software licence. With that in mind, and in order to ensure the code is not misused, I assert myself under the international copyright convention as the original author of the GEM Programmers’ Workbench, am releasing it for general use and interest, under the terms of the GNU General Public Licence. Enjoy!

Don’t ask me any questions about it, though; I’ve long since forgotten what went on in this code. My coding standards have improved one hell of a lot since then (well, I was young and spotty).

files

The workbench uses a number of files. These are:

  • BENCH.BAT    Compiles Workbench programs into a library (requires Lattice C compiler)
  • BENCH.DOC    Documentation
  • BENCH.H    Defines Workbench global variables
  • BTYPE.H    Defines Workbench constants and types, loads all the GEM include files, and defines the GEM global variables.
  • BINIT.C    contains starting and finishing functions
  • BDIAL.C    contains the one and only dialogue function
  • BTREE.C    contains tree processing functions
  • BWIND.C    contains window use and support functions
  • BMOUSE.C    contains mouse support functions
  • BWAIT.C    contains event functions

The sample application is a simple metafile decoder.

using the workbench

Introduction

If you decide to use the Workbench, then you should use all the workbench functions in preference to the equivalent GEM ones, since they tie together and tend to make assumptions about what other functions have done (in particular, the window contents may get mucked up if you are not careful).

You should start and finish your program using wb_start and wb_finish. Dialogues can be displayed and redisplayed using wb_dialogue. Windows can be handled using wb_open and wb_close. Events should be handled using wb_await, which will filter out window and mouse maintenance requirements and handle them.

A number of other facilities are provided, including tree analysis functions, window and desktop handling functions, and the ability to tie rectangles to windows for automatic processing of mouse movement rectangles associated with windows. Cop outs for low level processing are also provided.

The workbench also includes a number of “servicing” functions, which are not designed for use in other circumstances, so make sure you know what you are doing if you intend to use them. Certain functions may make assumptions about the state of operation which could become untrue if they are used in other circumstances.

NOTE: In the discussions below, the headers for the functions are reproduced. If you decide to declare Workbench functions as externals in your code, then simply copying these headers will cause syntax errors.

Starting and Stopping

To prepare for and depart from the Workbench (and the AES), use these two functions:

    WORD wb_start (resourcefile, menu, icons)
    char *resourcefile;
    WORD *icons;
    LONG *menu;

    wb_finish (start_result)
    int start_result;   

You must use wb_start before you use any other Workbench, or any GEM AES function. It prepares GEM for use, loads the resource file (which must be named), optionally turns on the menu bar, can convert your program’s icons and images to the local format, and prepares the Workbench variables. You will need to modify the Workbench constant READ_ERROR to actually be the read error number of your system.

If the pointer menu is NULL, then the function does not display the menu bar. Otherwise, the referenced value must be set to the RCS name of the menu (eg, PROGMENU, MYMENU, or whatever you called it). Wb_start will change this value to be the address of the root of the menu tree.

If icons is NULL, then wb_start does not attempt to convert any icons or images from Normalised Device Co–ordinates to Raster Co–ordinates. Otherwise, it should point to the root of a WORD array, which should consist of a sequence of tree name, item name pairs, and should be terminated by -1. For example, if your program has an icon called TRASHCAN in the tree DESKTOP, and an image called LOGO in the tree called ABOUT, then you would pass the address of an array containing { ABOUT, LOGO, DESKTOP, TRASHCAN, -1 } for this parameter. The function is able to determine whether a particular object is an icon or image. If you do not convert your icons and images in this or some other way, then they appear corrupt when displayed.

The function sets the following global variables:

          __vdi_handle,
          __cellw, __cellh,

__vdi_handle is set to a virtual workstation used by the Workbench for some low level VDI activities. If you want to use a VDI virtual screen workstation, open your own; do not use this handler. The other two globals, __cellw and __cellh, are the width and height respectively of a character cell, as obtaining using the AES function graf_handle.

The function returns the application number of your program, or a negative number when something has gone wrong.

Wb_finish should be the last Workbench or AES function called in your program. It closes any currently open window, removes the menu bar if it was displayed by wb_start, recovers the space occupied by the resource file, and “logs out” of the AES. It only takes one parameter, the number returned by wb_start (so it can handle the possible wb_start error situations cleanly).

Alerts

There is no special Workbench alert function. You should call them up in the usual way, using form_alert (), and, if the alert comes from your resource file, rsrc_gaddr ().

Dialogues

The dialogue function provided in the Workbench is derived from the one found in the GEM demonstration program. The function is:

    WORD wb_dialogue (control, dialogue, start_object)
    WORD start_object, control;
    LONG dialogue; 

The function displays a dialogue, and permits user interaction with it. It does not draw grow and shrink boxes, since they are due to be withdrawn from forthcoming releases of GEM. The function can support partial calls, so that the program can update the dialogue in response to the user’s answers. It does not handle display only dialogues, since they simply require a call to objc_draw (), optionally preceded by a call to form_center () (note the mis–spelling of this function).

The first parameter is one of:

  • WB_START start the dialogue
  • WB_CONTINUE continue it!
  • WB_REDRAW redraw dialogue and continue
  • WB_END finish it off
  • WB_ALL Start and finish

If no interaction is to occur between the user and the program whilst the dialogue is displayed, use WB_ALL. Otherwise, start the dialogue off by using WB_START. This will return with the dialogue still displayed on the screen. You should then either leave the dialogue by recalling the function with this parameter set to WB_END, or update the dialogue and use either WB_REDRAW or WB_CONTINUE. If you have not changed the dialogue, use WB_REDRAW, otherwise use WB_CONTINUE. The latter is visually more efficient, since you can simply redraw the object(s) that need updating, whereas WB_REDRAW will redraw the entire dialogue, which may look untidy.

The next parameter is the address of the root of the dialogue. This can be obtained with the function rsrc_gaddr () (the dialogue is a tree). The final parameter is the object in which which the text cursor is to appear when the dialogue is drawn. The function returns the exit box chosen by the user, or minus one in the case of WB_END.

Remember that GEM leaves dialogue trees updated by the user. If you wish to redisplay the dialogue, you should at least deselect the exit box chosen by the user using the function wb_state, and, if necessary, reset the default answers using the function wb_default (both defined below).

Trees

There are a number of tree handling functions. Trees are the main structure used by the AES to define the shape of things to draw, and they also contain the answers to any questions the user may have provided. They are:

    BOOLEAN wb_switch (tree, item, state)
    LONG tree;
    WORD item, state;

    WORD wb_find (tree, start_object, state, maxdepth)
    LONG tree;
    WORD start_object, state, maxdepth;

    VOID wb_state (tree, item, state, new_state)
    LONG tree;
    WORD item, state, new_state;

    WORD wb_reply (tree, item, reply)
    LONG tree;
    WORD item;
    BYTE *reply;

    VOID wb_default (tree, item, def)
    LONG tree;
    WORD item;
    BYTE *def;

Wb_switch returns the current state of a particular switch of an object in a tree. Wb_find searches for the first or next object in a tree which has a particular state set. Wb_state changes the state of an object in a tree. Wb_reply gets the users reply to an editable text field in the tree. Finally, wb_default sets the default answer for an editable text field. The states accessed by these functions are those found in the OB_STATE field, which is defined in section six of the AES manual. I find I generally use them to see if an object has been selected.

In all cases, tree is the root of the tree being considered (which may, if necessary, be found using rsrc_gaddr), item is the particular item in the tree being processed, and state is the state being considered (usually SELECTED).

Wb_find requests -1 to be passed in place of the start item in a search when the next item is desired (using -1 with a different tree will cause havoc!). If the entire tree is to be searched, then maxdepth should be the standard GEM constant MAX_DEPTH, otherwise it should be whatever value you consider suitable. It returns the next item in the tree which has the specified state switch set.

Wb_state adjusts the state of a particular bit in the state field. The final parameter should be one of WB_SET, WB_RESET or WB_TOGGLE. See also the function wb_dstate, described with windows.

The items specified with wb_reply and wb_default must be TEDINFOs; that is, editable text fields or simply text fields. Wb_reply fills the third parameter with the user’s reply, and wb_default sets the text field to be the contents of the third parameter. I decided to avoid returning simple pointers to the relevant location since it could not be guaranteed that they would be within the range required by small mode operations on the 8086 so to maintain machine independence I used the copying approach instead.

Windows

The Workbench supports a number of window functions. They are:

    WORD wb_open (components,  control,  window_tree,  width,  height,
                  redraw_function, open_area, full_area,
                  horizontal_unit, vertical_unit, title, info_line)
    LONG   window_tree, width, height;
    WORD   components, control, (*redraw_function) (), 
           horizontal_unit, vertical_unit;
    GRECT *open_area, *full_area;
    BYTE  *title, *info_line;

    VOID wb_qdraw (which, area)
    WORD   which;
    GRECT *area;

    VOID wb_qopen (tree, title)
    LONG tree;
    BYTE *title;

    VOID wb_close (which)
    WORD which;

    VOID wb_redraw (which)
    WORD which;

    VOID wb_dstate (redrawcon, which, item, state, new_state)
    WORD redrawcon, which, item, state, new_state;

    VOID wb_hide (which)
    WORD which;

    VOID wb_show (which)
    WORD which;

    WORD wb_desktop (control, tree)
    WORD control;
    LONG tree;

Wb_open opens a window, and wb_close closes it. Wb_qopen opens a window with a lot less bother. Wb_qdraw is one of those redrawing functions needed by wb_open. Wb_redraw redraws the contents of a window. Wb_dstate is like wb_state, except it will also redraw the modified item. Wb_hide and wb_show respectively remove and redisplay an opened window. Finally, wb_desktop controls the contents of window zero, the desktop itself. They all use a window record structure, which is described at the end of this section.

The first parameter of wb_open is the window’s components, as defined in the AES manual. The second is another set of options, this time those needed by the Workbench. They are:

  • WB_HIDDEN Do not display window
  • WB_ACTIVATE Permit activation (double click) in this window
  • WB_SELECT Permit object selection (and deselection) in this window
  • WB_DRAGSELF Permit dragging within window (WB_SELECT should be chosen!)
  • WB_DRAGANY Permit dragging between this and other windows (WB_SELECT and, preferably, WB_DRAGSELF should be chosen!)
  • WB_LOWMOUSE disable mouse support functions
  • WB_LOWSUPPORT disable window support functions

Probably the most important parameter in the function is the next one, window_tree. This defines the contents of the window. If the window is going to be fully supported by the Workbench, it needs to have a good idea of the contents. The visible contents should be specified by this tree. If you do not use an OBJECT tree (such as a free tree) to specify the window contents, then the Workbench cannot support mouse activity, and you should choose WB_LOWMOUSE as one of the options specified above.

Whether or not you specify a tree, the Workbench needs to know the imaginary width and height of the window’s contents. For example, if your window is to contain an entire text file, then the height of it’s contents is the number of lines in the file multiplied by the cell height of a character. This information is essential for slider support. Note that these values are LONGs. If the entire contents of the window is contained in the specified window tree, then these values may be -1L, in which case the size of the tree is used.

The next parameter is the window redraw function. This can either be a function of your own design, or a reference to wb_qdraw. In the latter case, the complete contents of the window should be contained in the tree. If you decide to use your own functions, then the parameters it should expect are firstly the handle of the window to be redrawn, and secondly the clip rectangle of area of the screen to be redrawn (like wb_qdraw). The position of the contents of the window can be obtained from the field _theoretical_area, discussed below. This will be modified by the various window functions as the user changes the position and size of the window.

The parameter open_area defines the area to be used to display the window when it is first opened. If this pointer is NULL, then wb_open calculates a suitable value: approximately one ninth of the area of the screen, displayed centrally. The parameter full_area is similar, defining the maximum area of the window. If it is NULL, and a tree is specified, it sets this area to be the size of the tree displayed centrally on the work area of the screen.

The parameters horizontal_unit and vertical_units define the amount of window to be moved when the user clicks on the horizontal or vertical arrows. These values should be the size of a “unit” of the contents of the window. If these values are FALSE, then they default to the width and height of a character cell respectively.

The final parameters are the title and information line of the window. These should be static values. If you change them whilst the window is open, then the displayed title of the window will be changed accordingly as soon as the window needs to be redisplayed.

The function returns the window handler for the opened window, or a negative number if something has gone wrong. This handler is not the same as the AES window handler.

The function wb_qopen is designed to make it simple to open windows. It only takes two parameters; the contents of the window, and the title of the window. It returns the window’s handler.

The function wb_close closes a window, wb_hide removes a window from the screen without closing it, and wb_show puts a window back on the screen which was previously hidden (or opened with the control option WB_HIDDEN chosen). They all take one parameter, the handler of the window in question.

The function wb_redraw redraws the contents of a window. You should use this function in preference to your own, since it guarantees that only the visible part of the window is redrawn (it will, of course, call your function). It takes the window handler as its one parameter.

The function wb_desktop, in effect, opens a window for the background of the screen. It takes two parameters, a control WORD, which is the same as the control WORD for wb_open, although WB_HIDDEN is ignored. The second parameter is the tree to be displayed in the background. Wb_desktop will automatically set the position and size of this tree to fit the screen. It will not, however, set the background pattern to be the standard pattern so loved by GEM users. You can guarantee that the handler for this window will be zero.

The final function in this suite is wb_dstate, which can change the state of an object in a window and redraw it. This function is designed to be used when a number of objects need to be changed and the minimum necessary redraw is needed. It can build an area of the screen to redraw by combining the area of all the items modified, and draw them when all the modifications have taken place. Since drawing is slow, this is a very efficient way of prettily updating the screen, especially as the user does not see a flicker. The first parameter controls the nature of the redraw:

  • WB_DISPLAY Modify and redraw item.
  • WB_NEXT Modify, and add the item to the draw area
  • for subsequent redrawing.
  • WB_FINISH Display the draw area.
  • WB_INCLUDE Just add item to the draw area.

The next parameters are the window which contain the items to be redrawn, and the item itself. The final two parameters are the state to be modified and its new value, which are fed straight to a call of wb_state (defined above). This function could easily be modified to allow it to redraw portions of dialogues which are updated whilst displayed on the screen.

Naturally, a call using WB_FINISH does not require an item to be specified. WB_INCLUDE and WB_NEXT do not cause a redraw; they add the specified item to the list. WB_FINISH displays the combined area to be redrawn, and forgets it, so a subsequent call using WB_NEXT or WB_INCLUDE start things off again.

The Workbench window functions are built around a window structure. One occurrence of the structure is associated with each window opened. This structure is:

    typedef struct /* window_struct */ {

      WORD  _handler,               AES window handler
            _components,            window’s components
            (*_drawer) (),          draw function
            _control,               window control bits
            _which_rectangle [2],   which rectangle being awaited
            _intersect [2],         is rectangle to intersect work area?
            _mouseform [2],         mouse form when inside rectangle
            _full_size,             TRUE if currently full size
            _up_move, _across_move; distances moved with arrows
      GRECT _old,                   old or hidden window size
            _rectangle [2];         awaited rectangle
      LONG  _contents;              window’s display tree
      LONG_GRECT _theoretical_area; “area’ of contents of window

    } window_struct;

The type LONG_GRECT is unique to the Workbench, and is identical to the GRECT structure except it uses LONGs. It is:

    typedef struct /* LONG_GRECT */ {     This structure is  equivalent
                                          to the standard GRECT, except
      LONG l_x, l_y, l_w, l_h;            its   for   big   (imaginary)
                                          rectangles
    } LONG_GRECT;

Most of these fields should become obvious after reading the notes on window functions, and studying the source code.

Events

The Workbench provides a high level event structure with these functions:

    WORD wb_await (wb_events, mesaddr, time, current_window,
                   source_object, target_window, target_object,  menu, 
                   item, dragkeys, key)
    WORD wb_events, *mesaddr, *current_window, *source_object,
         *target_window, *target_object, *menu, *item, *dragkeys,
         *key;
    LONG time;

    VOID wb_rectangle (which, quel_rectangle, choice, area,
                       do_intersect, mouse_in)
    WORD   which, quel_rectangle, choice, do_intersect, mouse_in;
    GRECT *area;

Wb_await awaits for one of a particular set of events to occur. If you have any windows open then it will filter out window support messages and deal with them directly, unless you specify otherwise.

It can await the following events:

  • WB_1ENTER Enter rectangle 1
  • WB_1LEAVE Leave rectangle 1
  • WB_2ENTER Enter rectangle 2
  • WB_2LEAVE Leave rectangle 2
  • WB_ACTIVATE Double click on an object
  • WB_DRAG Objects dragged
  • WB_CLOSE Window closed
  • WB_MENU Menu item selected
  • WB_DELAY Minimum timer delay
  • WB_KEYBOARD Character typed in
  • WB_MESSAGE Unknown message event
  • WB_SELECT Object(s) selected
  • WB_DESELECT Object(s) deselected
  • WB_MOUSE Mouse event in low level wind
  • WB_SUPPORT Low level window support event

The variable referred to by the pointer parameter will always be set to the identity of the window currently on top, or -1 if there isn’t one.

WB_MENU occurs when the user selects a menu item. In this case, the variables referred to by the pointer parameters menu and item are set to the particular option chosen by the user.

WB_DELAY will return after the period of time specified by the parameter time has passed. Note that this is a minimum time delay. Since wb_await can process events without your program’s knowledge it is possible that a much longer period than that specified may occur before return. The function can be modified to overcome this shortcoming by making direct calls to DOS to get the current time; however, this would unfortunately make it far more machine dependant.

WB_KEYBOARD returns when a key is pressed by the user. It sets the variable referenced by the parameter key accordingly.

WB_CLOSE returns when the user has attempted to close the window currently on top. It will not close the window.

The rectangle events WB_1ENTER, WB_1LEAVE, WB_2ENTER and WB_2LEAVE are associated with the window currently on top. They can be specified using the function wb_rectangle, discussed below. Note that if a mouse form is associated with a particular rectangle which is not the standard arrow, then wb_await will automatically set that form when the mouse enters a rectangle whether or not the rectangle events are awaited. The second mouse form takes priority over the first when the mouse is in an area which is in both rectangles, although not very neatly.

WB_MESSAGE may be returned when an unknown message is received by the function. Since all standard messages, except desk accessory ones, are dealt with by the function, then this should only occur when another process attempts to communicate with yours (or when you’re using thoroughly incompatible versions of GEM and the Workbench!).

WB_MOUSE and WB_SUPPORT return when a mouse button event or a window support message is received, and the window currently on top is opened requiring those events to be returned. They effectively disable wb_await’s high level facilities.

WB_ACTIVATE, WB_DRAG, WB_SELECT and WB_DESELECT can only be applied to windows which have an object tree associated with them. WB_SELECT and WB_DESELECT only inform your program that a selection or deselection has taken place; if you do not specify them, the user can still select items in a window (provided that window permits it) but your program won’t be told about it. The user can carry out selections by simply clicking on an object, or by using rubberboxes. The shift key operates in the same manner as the desktop, except that it can also be used with rubberboxes. WB_ACTIVATE occurs when the user double clicks on an object. It sets the variable referred to by the pointer parameter source_object accordingly. WB_DRAG occurs when the user drags objects. It sets the variables referred to by the parameters target_window, target_object and dragkeys accordingly. Dragkeys is set to specify which keys out of the two shift keys, control and alternate were depressed at the beginning of the drag, in the standard AES format. It only visually drags items already selected by the user.

When a drag occurs, wb_await also sets the following global variables:

extern GRECT __drag_start, start of drag area __drag_finish; finish of drag area extern WORD __mousex, __mousey; mouse position

__drag_start and __drag_finish are GRECT records specifying the area on the screen which need to be redrawn as a result of a drag if the dragged objects are to go precisely where the user put them. A simple function which should not be used to visually resolve a drag is below. It should be avoided for the reasons specified in the comments on multi–window dragging (it was designed for use with trees in windows which were defined using the Resource Construction Set). This function uses a number Workbench functions described elsewhere in this file.

    drag (windchsn)
    WORD windchsn; 

    { WORD i, j, objcx, objcy;  /* current position */
      GRECT second;             /* second window work area */
      LONG tree, k;

      if (windchsn < 0)
        return;

      tree = __window [curwin /* GLOBAL current window */]._contents;

      /* drag within one window ... */

      if (windchsn == curwin) {

        /* go through all selected items and change their location */

        i = wb_find (tree, 0, SELECTED, MAX_DEPTH);

        while (i >= 0) {

          wb_dstate (WB_INCLUDE, curwin, i, 0, 0);
          objcx = LWGET (OB_X (i)) - __drag_start.g_x + __drag_finish.g_x;
          objcy = LWGET (OB_Y (i))  - __drag_start.g_y +  __drag_finish.g_y;
          LWSET (OB_X (i), objcx);
          LWSET (OB_Y (i), objcy); 
          wb_dstate (WB_NEXT, curwin, i, SELECTED, WB_RESET);
          i = wb_find (tree, -1,  SELECTED, MAX_DEPTH);
       
       } 

       wb_dstate (WB_FINISH, curwin, 0,  0, 0);

     } else { /* multi–window drag (this  is a slight cheat) */

       /* find out  about the second window */
       __get_window (windchsn, WF_WXYWH, &second);

      /* go through all the items and bung them across windows */
      i =  wb_find  (tree, 0, SELECTED, MAX_DEPTH);

      while (i >  0) {

          j = wb_find (tree, -1, SELECTED, MAX_DEPTH);

          /* get memory location of item */

          objc_offset (tree, i, &objcx, &objcy);

          /* Calculate the location of the item in memory compared to the 
             target window’s root. This is quite  naughty, since this can 
             result  in  negative  numbers for positions of siblings etc. 
             in the OBJECT field.  Since  -1  means no such entry,  it is 
             possible to cause a  slight  bit  of  confusion  using  this 
             technique.  If  you wish to seriously transfer stuff between 
             windows,  ensure your window object tree  has  enough  empty
             spaces  to  cater  for all possible transfers,  and copy the 
             object details  (including siblings),  into  suitable  blank 
             spaces.                                                      */

          k = (OB_NEXT (i) - __window [windchsn]._contents) / sizeof (OBJECT);
          wb_state (tree, i, SELECTED, WB_RESET);

          /* remove from source tree and add to target (at new position) */

          objc_delete (tree, i);
          tree = __window [windchsn]._contents;
          objcx += __drag_finish.g_x - __drag_start.g_x - second.g_x;
          objcy += __drag_finish.g_y - __drag_start.g_y - second.g_y;
          objc_add (tree, 0, (WORD) k);
          LWSET (OB_X (k), objcx);
          LWSET (OB_Y (k), objcy);
          tree = __window [curwin]._contents;
          i = j;

        }

        /* __gredraw redraws using GRECTs as opposed to x, y, w, h */

        __gredraw (curwin, &__drag_start);
        __gredraw (windchsn, &__drag_finish);

      }

    }

The function returns those events which have occurred. Note that certain events are not necessarily unique, so do not expect them to be so.

Wb_rectangle refines the rectangles which may be awaited by wb_await. Each window may have two rectangles associated with it; the particular rectangles awaited are those associated with the window currently on top. In the Workbench as implemented, this excludes window zero (the desktop) when another window is open.

The first parameter is the handle of the window chosen (see the section on windows below). The second parameter is one or two, specifying which rectangle is being set. The third is one of the following control options:

  • WB_WORK The window’s work area
  • WB_CURRENT The window’s current area
  • WB_OBJECT An item in the window’s tree
  • WB_ROOT An area relative to the root of the window’s tree
  • WB_ABSOLUTE A location on the screen

The fourth parameter is the actual rectangle desired, which will be related to the area specified by the control option. In the case of WB_OBJECT, this must not be NULL, and the x value is the number of the object in the window’s tree. Otherwise, this may be NULL, in which case the entire rectangle specified by the control parameter is applied, or, if not NULL, the specified rectangle is located relative to the root of the control parameter rectangle.

The fifth parameter is TRUE or FALSE, depending on whether or not you wish the calculated area to be within or without the window’s work area. Finally, the last parameter is the standard mouse form to be used when the mouse is in the rectangle.


All GEM software, except the GEM Programmers’ Workbench, is © Lineo Inc.. It is released by them as free software under the terms of the GNU General Public Licence, as published by the Free Software Foundation. A copy of the licence is included with the software.

ancient front