Beginner's Guide to WIMP
Programming
Martyn Fox

21: A Wimp Shell Program

Section 10 of this guide introduced a simple Wimp shell program: a collection of useful functions and procedures that could easily be adapted to form the basis of a new multi-tasking application with minimal effort. That first version of the shell program was based on what we had learnt to date in creating our !Test application, and itself formed the basis of our more sophisticated Shapes application.

Now that the Shapes application is complete, we can take some of the new functions and procedures that it contains and put them back into our shell in order to make it a more useful basis for future applications. Of course, as you become experienced in writing your own software, you will almost certainly wish to supplement the new shell with futher routines of your own.

If you need to refresh your memory about the shell program, reread Section 10; what follows here will supplement the simple shell program by extending the contents of its !RunImage file, but the other related files will not change.

Creating the Shell Program

If your !RunImage file is similar to the final version listed in the Appendix, you've already done most of the work as all you need to do is delete some of it, make a few changes and add a few procedures and functions (which are optional) on the end. First, save a copy of the final !RunImage from the Shapes application in the application directory for your new shell, so that you don't destroy your final copy of the Shapes program by accident!

You will need to go through your !RunImage file, comparing it with the listing at the end of this section, making appropriate deletions, additions and changes. You will find that there are differences in some of the function and procedure definitions, so check carefully. There are a few new (optional) ones at the end for you to enter, but any functions and procedures that are not listed as part of the shell should be deleted, as they were used by the Shapes application and are no longer needed.

You may wish to use your editor's Find/Replace facility to replace all instances of 'Shapes' with 'Shell', though note that the instance in PROCcreateicon (line 200) has a lower-case 's' as it is the name of a sprite.

Each function or procedure begins with a REM statement to remind you of what it does - you can, of course, leave them out if you wish.

PROCpoll contains a CASE ... OF structure to handle the reason codes you are almost certain to meet on return from Wimp_Poll, including reason code 1, a call to PROCredraw. If all your windows contain only icons, you can delete this line, together with PROCredraw itself.

We've removed the mask from R0 when calling Wimp_Poll, as you will need to create one to suit your own application. This was covered at the end of Section 17. Unless your program needs to respond to reason code zero (the null reason code), you should be sure to mask it out by putting a number in R0 with bit zero set (such as 1).

Error Handler Modifications

The error handler has been expanded to check for three error numbers. The number 1<<30 produces an error box with both 'OK' and 'Cancel' icons, (1<<30)+1 produces a box with just the 'OK' icon and (1<<30)+2 one with just the 'Cancel' icon. This last one enables you to produce a fatal error without having to show the line number in the error message.

PROCreceive now handles only the 'Close Down' message. You can add your own lines if you need to handle messages for loading and saving files.

PROCmouseclick has been reduced so that it caters only for Menu- and Select-clicks on the icon bar, and for clicks in the main window, which are handled by PROCwindow_click.

PROCget_origin works out the coordinates of a window's work area origin and returns them, thanks to the RETURN keywords before xorig% and yorig%.

PROCload_templates is only used to load and create the Info box and main window, but the lines that do this can be copied, with modifications, to load other windows.

Line 1330 puts the version string into the appropriate icon's indirected data. This line assumes that the 'version number' icon is icon number 4. You should check this in your template editor and either renumber the icons or alter the line if necessary. You should also check that the length of the icon's indirected data buffer is sufficient for your likely version number.

The menu creation routine, FNmake_menu, is basically as we wrote it earlier. We've included a couple of checks to ensure that the total size of our menus does not exceed the amount of memory we set aside for the purpose - we don't want to start overwriting other data!

PROCredraw has been included as it's a fairly standard routine for coping with requests from the Wimp to redraw a window. The procedure that it calls, though, PROCdraw, consists entirely of REM statements, because it has to be specially written for each individual application. Note that the window handle, in b%, is passed to PROCdraw; this is in case you have several windows that need redrawing, so that PROCdraw can distinguish between them.

More Useful PROCs and Functions

PROCterm simply puts a Return character - ASCII code 13 - on the end of any string starting at a%. This is necessary if the string has been produced by RISC OS, because it may end with a zero. If you want Basic to use the string, it has to end with a Return.

PROCdrag is included in case you want to drag an icon, for example at the start of a save operation. This procedure starts a drag of type 5, which means a box of rotating dashes. The initial position of the box is taken from the bounding box of the icon where you held down the mouse button.

FNicon_state checks whether or not a particular icon is selected, and returns TRUE if it is. This function makes dialogue boxes particularly easy to use. You don't have to worry about icons being selected or deselected, or 'radio' type icons cancelling each other through their exclusive selection group - the Wimp takes care of all that. You simply have to check the state of the icons when they have some relevance to what your program does.

PROCforce_redraw takes the screen coordinates of the visible part of a window and forces the Wimp to redraw that rectangle. This is a simple way of making any changes to a window instantly visible.

FNwind_open simply returns TRUE if a window is open.

PROCpush simulates clicking the mouse over a particular icon. You could use this, for example, to make the default action button of a dialogue box appear to slab in when you press Return, just before the box vanishes from the screen. Note, though, that this routine is of most interest for programs that run on older hardware, because on fast machines the window is likely to close so quickly that you won't have time to see the slabbing-in effect. As explained in Section 20, considering the fact that this procedure uses a discouraged technique to produce its effect (albeit the only way of achieving it), you may prefer not to make use of PROCpush in your own programs.

Additional Routines

We've included several short routines that were not in our example programs, but which can be useful when handling icons and menus.

FNtick checks whether or not a menu item is ticked, and returns TRUE if it is; FNtoggle_tick changes the ticked or unticked state of an item; PROCgrey disables a menu item by turning it grey and PROCungrey reverses the process. These procedures work by altering the appropriate bits in the menu item flags.

FNstring_addr returns the address of an indirected icon's text string; FNicon_string uses this to return the actual string and PROCset_icon_string puts a string into the icon's text buffer. Note that these routines do not check to ensure that the icon does have indirected text - it's up to you to ensure that it does when you design your window. Note also that PROCset_icon_string doesn't check the size of the icon's buffer before putting a string into it. Again, it's up to you to ensure that enough space is available (or alter the routines to make them more foolproof).

FNindirect puts a string into the indirected data workspace and returns its address; very useful when creating icons with indirected data.

No doubt you can think of many other useful routines to include in your shell.

When you write your own application, you would probably do best to keep all these routines intact at first, until you have finally finished and debugged your program. Then you can delete any that you haven't used, and reduce the sizes of the data blocks at b%, ws% and menspc% to whatever your program needs.

It is now up to you and your inventiveness to see what you can get out of Wimp programming. You will find that there is something very satisfying about producing an application that has the same 'feel' as expensive, commercially produced software.

And have fun!

   10 REM >!RunImage
   20 REM (C) Martyn Fox
   30 REM Wimp shell program
   40 version$="0.01 (date)"
   50 ON ERROR PROCclose:REPORT:PRINT" at line ";ERL:END
   60 SYS "Wimp_Initialise",200,&4B534154,"Shell" TO ,task%
   70 PROCinit
   80 PROCcreateicon
   90 ON ERROR IF FNerror THEN PROCclose:END
  100 REPEAT
  110   PROCpoll
  120 UNTIL quit%
  130 PROCclose
  140 END
  150 :
  160 DEFPROCcreateicon
  170 REM creates the application's icon and puts it on the icon bar
  180 REM b%+0 = -1 for right side and -2 for left side
  190 !b%=-1:b%!4=0:b%!8=0:b%!12=68:b%!16=68:b%!20=&3002
  200 $(b%+24)="!shell":SYS"Wimp_CreateIcon",,b% TO i%
  210 ENDPROC
  220 :
  230 DEFPROCclose
  240 REM tells the Wimp to quit the application
  250 SYS "Wimp_CloseDown",task%,&4B534154
  260 ENDPROC
  270 :
  280 DEFPROCpoll
  290 REM main program Wimp polling loop
  300 SYS "Wimp_Poll",,b% TO r%
  310 CASE r% OF
  320   WHEN 1:PROCredraw
  330   WHEN 2:SYS "Wimp_OpenWindow",,b%
  340   WHEN 3:SYS "Wimp_CloseWindow",,b%
  350   WHEN 6:PROCmouseclick
  360   WHEN 8:PROCkeypress
  370   WHEN 9:PROCmenuclick
  380   WHEN 17,18:PROCreceive
  390 ENDCASE
  400 ENDPROC
  410 :
  420 DEFPROCmouseclick
  430 REM handles mouse clicks in response to Wimp_Poll reason code 6
  440 REM b%!0=mousex,b%!4=mousey:b%!8=buttons
      b%!12=window handle (-2 for icon bar):b%!16=icon handle
  450 CASE b%!12 OF
  460   WHEN -2:
  470     CASE b%!8 OF
  480       WHEN 2:PROCshowmenu(mainmenu%,!b%-64,96+2*44):
                   REM replace '2' with number of main menu items
  490       WHEN 4:!b%=main%:SYS "Wimp_GetWindowState",,b%:
                   SYS "Wimp_OpenWindow",,b%
  500     ENDCASE
  510   WHEN main%:PROCwindow_click
  520 ENDCASE
  530 ENDPROC
  540 :
  550 DEFPROCget_origin(handle%,RETURN xorig%,RETURN yorig%)
  560 REM returns coordinates of window work area origin
  570 LOCAL c%
  580 c%=FNstack(36)
  590 !c%=handle%
  600 SYS "Wimp_GetWindowState",,c%
  610 xorig%=c%!4-c%!20:yorig%=c%!16-c%!24
  620 PROCunstack(c%)
  630 ENDPROC
  640 :
  650 DEFFNstack(size%)
  660 REM allocates temporary memory from stack block
  670 REM stack must be cleared after use with PROCunstack
  680 IF stackptr%+size%>stackend%  ERROR 1,"No room in stack"
  690 stackptr%+=size%
  700 =stackptr%-size%
  710 :
  720 DEFPROCunstack(old_ptr%)
  730 REM removes temporary memory from stack
  740 stackptr%=old_ptr%
  750 IF stackptr%<stack% stackptr%=stack%
  760 ENDPROC
  770 :
  780 DEFFNmake_menu
  790 REM creates menu block from DATA statements
  800 LOCAL start%,title$,item$,ul%,tail$,writable%,buffer%,buflen%
  810 IF menspc%+28>menend% ERROR (1<<30)+2,"Not enough menu space"
  820 start%=menspc%
  830 READ title$
  840 $(start%)=title$
  850 start%?12=7:REM title foreground colour
  860 start%?13=2:REM title background colour
  870 start%?14=7:REM work area foreground colour
  880 start%?15=0:REM work area background colour
  890 start%!20=44:REM height of menu items
  900 start%!24=0:REM gap between items
  910 width%=LEN(title$)-3
  920 menspc%+=28
  930 REPEAT
  940   READ item$
  950   IF item$<>"*" THEN
  960     IF menspc%+24>menend% ERROR (1<<30)+2,"Not enough menu space"
  970     !menspc%=0
  980     writable%=FALSE
  990     ul%=INSTR(item$,"_")
 1000     IF ul% THEN
 1010       tail$=RIGHT$(item$,LEN(item$)-ul%)
 1020       IF INSTR(tail$,"T") !menspc%=!menspc% OR 1:REM tick
 1030       IF INSTR(tail$,"D") !menspc%=!menspc% OR 2:REM dotted line
 1040       IF INSTR(tail$,"W") !menspc%=!menspc% OR 4:writable%=TRUE:
            READ buffer%:READ buflen%:REM writable icon
 1050       IF INSTR(tail$,"M") !menspc%=!menspc% OR 8:REM generate message
 1060       item$=LEFT$(item$,ul%-1)
 1070     ENDIF
 1080     IF LENitem$>width% width%=LENitem$
 1090     menspc%!4=-1:REM submenu ptr
 1100     IF writable% THEN
 1110       menspc%!8=&0700F121:menspc%!12=buffer%:
            menspc%!16=-1:menspc%!20=buflen%:$buffer%=item$
 1120       ELSE
 1130       IF LENitem$<12 THEN
 1140         menspc%!8=&07000021:$(menspc%+12)=item$
 1150         ELSE
 1160         menspc%!8=&07000121:menspc%!12=ws%:menspc%!16=-1:menspc%!20=LENitem$+1
 1170         $ws%=item$:ws%+=LENitem$+1
 1180       ENDIF
 1190     ENDIF
 1200     menspc%+=24
 1210   ENDIF
 1220 UNTIL item$="*"
 1230 start%!16=width%*16+32
 1240 !(menspc%-24)=!(menspc%-24) OR &80
 1250 mptr%=menspc%
 1260 =start%
 1270 :
 1280 DEFPROCload_templates
 1290 REM opens window template file, loads and creates window
 1300 SYS "Wimp_OpenTemplate",,"<Shell$Dir>.Templates"
 1310 REM ****** load and create Info box ******
 1320 SYS "Wimp_LoadTemplate",,stack%,ws%,wsend%,-1,"progInfo",0 TO ,,ws%
 1330 $stack%!(88+32*0+20)=version$
 1340 SYS "Wimp_CreateWindow",,stack% TO info%
 1350 REM ****** load and create main window ******
 1360 SYS "Wimp_LoadTemplate",,stack%,ws%,wsend%,-1,"Main",0 TO ,,ws%
 1370 SYS "Wimp_CreateWindow",,stack% TO main%
 1380 REM ****** end of window creation ******
 1390 SYS "Wimp_CloseTemplate"
 1400 ENDPROC
 1410 :
 1420 DEFPROCattach(menu%,item%,sub%)
 1430 REM attach submenu or dialogue box to main menu
 1440 !(menu%+28+item%*24+4)=sub%
 1450 ENDPROC
 1460 :
 1470 DEFPROCinit
 1480 REM initialisation before polling loop starts
 1490 DIM b% 255,ws% 1023,menspc% 1023,stack% 1023
 1500 wsend%=ws%+1024:menend%=menspc%+1024:stackend%=stack%+1024:stackptr%=stack%
 1510 quit%=FALSE
 1520 PROCload_templates
 1530 PROCmenus
 1540 ENDPROC
 1550 :
 1560 DEFPROCreceive
 1570 REM handles messages received from the Wimp with reason codes 17 or 18
 1580 CASE b%!16 OF
 1590   WHEN 0:quit%=TRUE
 1600 ENDCASE
 1610 ENDPROC
 1620 :
 1630 DEFPROCkeypress
 1640 REM processes keypresses in response to Wimp_Poll reason code 8
 1650 LOCAL key%
 1660 key%=b%!24
 1670 CASE key% OF
 1680   OTHERWISE
 1690     SYS "Wimp_ProcessKey",key%
 1700 ENDCASE
 1710 ENDPROC
 1720 :
 1730 DEFPROCwindow_click
 1740 REM handles mouse clicks on window
 1750 REM b%!0=mousex,b%!4=mousey:b%!8=buttons:
      b%!12=window handle (-2 for icon bar):b%!16=icon handle
 1760 VDU 7
 1770 ENDPROC
 1780 :
 1790 DEFPROCmenus
 1800 REM create menus and attach submenus and dialogue boxes
 1810 PROCmain_menu
 1820 PROCattach(mainmenu%,0,info%)
 1830 ENDPROC
 1840 :
 1850 DEFPROCshowmenu(menu%,x%,y%)
 1860 REM opens menu at given coordinates
 1870 topmenu%=menu%:topx%=x%:topy%=y%
 1880 SYS "Wimp_CreateMenu",,menu%,x%,y%
 1890 ENDPROC
 1900 :
 1910 DEFPROCmenuclick
 1920 REM handles mouse clicks on menu in response to Wimp_Poll reason code 9
 1930 LOCAL c%,adj%
 1940 c%=FNstack(20)
 1950 SYS "Wimp_GetPointerInfo",,c%
 1960 adj%=(c%!8 AND 1)
 1970 SYS "Wimp_DecodeMenu",,topmenu%,b%,c%
 1980 CASE $c% OF
 1990   WHEN "Quit":quit%=TRUE
 2000 ENDCASE
 2010 IF adj% PROCshowmenu(topmenu%,topx%,topy%)
 2020 PROCunstack(c%)
 2030 ENDPROC
 2040 :
 2050 DEFPROCmain_menu
 2060 REM creates main menu, calling FNmake_menu
 2070 RESTORE +1
 2080 DATA Shell,Info,Quit,*
 2090 mainmenu%=FNmake_menu
 2100 ENDPROC
 2110 :
 2120 DEFPROCredraw(b%)
 2130 REM redraws window contents
 2140 LOCAL xorig%,yorig%,more%
 2150 PROCget_origin(!b%,xorig%,yorig%)
 2160 SYS "Wimp_RedrawWindow",,b% TO more%
 2170 WHILE more%
 2180   PROCdraw(b%,xorig%,yorig%)
 2190   SYS "Wimp_GetRectangle",,b% TO more%
 2200 ENDWHILE
 2210 ENDPROC
 2220 :
 2230 DEFPROCdraw(b%,xorig%,yorig%)
 2240 REM called when all or part of window needs redrawing
 2250 REM xorig% and yorig% are coordinates of work area origin 
      (top left-hand corner of window work area)
 2260 REM b% points to block:
 2270 REM b%!0  : window handle
 2280 REM b%!4  : visible area minimum x coordinate
 2290 REM b%!8  : visible area minimum y coordinate
 2300 REM b%!12 : visible area maximum x coordinate
 2310 REM b%!16 : visible area maximum y coordinate
 2320 REM b%!20 : scroll x offset relative to work area origin
 2330 REM b%!24 : scroll y offset relative to work area origin
 2340 REM b%!28 : current graphics window minimum x coordinate
 2350 REM b%!32 : current graphics window minimum y coordinate
 2360 REM b%!36 : current graphics window maximum x coordinate
 2370 REM b%!40 : current graphics window maximum y coordinate
 2380   :
 2390 REM Fill in as necessary!
 2400 ENDPROC
 2410 :
 2420 DEFFNicon_state(window%,icon%)
 2430 REM chacks if icon is selected and returns TRUE or FALSE
 2440 LOCAL c%
 2450 c%=FNstack(56)
 2460 !c%=window%:c%!4=icon%
 2470 SYS "Wimp_GetIconState",,c%
 2480 PROCunstack(c%)
 2490 =((c%!24) AND (1<<21))<>0
 2500 :
 2510 DEFPROCforce_redraw(window%)
 2520 REM forces redraw of the entire screen rectangle covering a window's contents
 2530 LOCAL c%
 2540 c%=FNstack(36)
 2550 !c%=window%
 2560 SYS "Wimp_GetWindowState",,c%
 2570 SYS "Wimp_ForceRedraw",-1,c%!4,c%!8,c%!12,c%!16
 2580 PROCunstack(c%)
 2590 ENDPROC
 2600 :
 2610 DEFFNerror
 2620 REM main error handling routine
 2630 REM returns TRUE if Cancel box clicked
 2640 !b%=ERR
 2650 CASE !b% OF
 2660   WHEN 1<<30:err_str$="":box%=3
 2670   WHEN (1<<30)+1:err_str$="":box%=1
 2680   WHEN (1<<30)+2:err_str$="":box%=2
 2690   OTHERWISE:err_str$=" at line "+STR$ERL:box%=2
 2700 ENDCASE
 2710 $(b%+4)=REPORT$+err_str$+CHR$0
 2720 SYS "Wimp_ReportError",b%,box%,"Shell" TO ,response%
 2730 =(response%=2)
 2740 :
 2750 DEFPROCterm(a%)
 2760 REM replaces terminator on end of string starting at a% with Return character
 2770 LOCAL n%
 2780 WHILE a%?n%>31
 2790   n%+=1
 2800 ENDWHILE
 2810 a%?n%=13
 2820 ENDPROC
 2830 :
 2840 DEFPROCdrag(window%,icon%)
 2850 REM creates drag box to fit icon and starts drag operation
 2860 LOCAL c%
 2870 c%=FNstack(56)
 2880 PROCget_origin(window%,xorig%,yorig%)
 2890 !c%=window%:c%!4=icon%
 2900 SYS "Wimp_GetIconState",,c%
 2910 xmin%=xorig%+c%!8:ymin%=yorig%+c%!12:xmax%=xorig%+c%!16:ymax%=yorig%+c%!20
 2920 c%!4=5:REM drag type
 2930 c%!8=xmin%:REM coordinates of drag box
 2940 c%!12=ymin%
 2950 c%!16=xmax%
 2960 c%!20=ymax%
 2970 c%!24=0:REM screen min x
 2980 c%!28=0:REM screen min y
 2990 c%!32=1280:REM screen max x
 3000 c%!36=1024:REM screen max y
 3010 SYS "Wimp_DragBox",,c%
 3020 PROCunstack(c%)
 3030 ENDPROC
 3040 :
 3050 DEFFNwind_open(h%)
 3060 REM returns TRUE if a window is open
 3070 LOCAL c%
 3080 c%=FNstack(36)
 3090 !c%=h%
 3100 SYS "Wimp_GetWindowState",,c%
 3110 PROCunstack(c%)
 3120 =(c%!32 AND 1<<16)<>0
 3130 :
 3140 DEFPROCpush(w%,i%)
 3150 REM simulates clicking of a push-button icon
 3160 LOCAL c%
 3170 PROCget_origin(w%,xorig%,yorig%)
 3180 c%=FNstack(56)
 3190 !c%=w%:c%!4=i%:SYS "Wimp_GetIconState",,c%
 3200 x%=xorig%+c%!8:y%=yorig%+c%!12
 3210 SYS "OS_ReadMonotonicTime" TO t%
 3220 SYS "OS_Byte",138,9,(x%+20) MOD 256
 3230 SYS "OS_Byte",138,9,(x%+20) DIV 256
 3240 SYS "OS_Byte",138,9,(y%+20) MOD 256
 3250 SYS "OS_Byte",138,9,(y%+20) DIV 256
 3260 SYS "OS_Byte",138,9,4
 3270 SYS "OS_Byte",138,9,t% MOD 256
 3280 SYS "OS_Byte",138,9,(t% DIV &100) MOD 256
 3290 SYS "OS_Byte",138,9,(t% DIV &10000) MOD 256
 3300 SYS "OS_Byte",138,9,(t% DIV &1000000) MOD 256
 3310 PROCunstack(c%)
 3320 ENDPROC
 3330 :
 3340 DEFFNtick(menu%,item%)
 3350 REM returns TRUE if menu item ticked
 3360 =(menu%!(28+item%*24) AND 1)<>0
 3370 :
 3380 DEFPROCtoggle_tick(menu%,item%)
 3390 REM reverses state of tick on menu item
 3400 menu%!(28+item%*24)=menu%!(28+item%*24) EOR 1
 3410 ENDPROC
 3420 :
 3430 DEFPROCgrey(menu%,item%)
 3440 REM turns menu item grey to disable it
 3450 menu%!(28+item%*24+8)=(menu%!(28+item%*24+8)) OR (1<<22)
 3460 ENDPROC
 3470 :
 3480 DEFPROCungrey(menu%,item%)
 3490 REM enables menu item and removes greyness
 3500 menu%!(28+item%*24+8)=(menu%!(28+item%*24+8)) AND (NOT (1<<22))
 3510 ENDPROC
 3520 :
 3530 DEFFNstring_addr(window%,icon%)
 3540 REM returns address of icon's indirected text string
 3550 LOCAL c%
 3560 c%=FNstack(56)
 3570 !c%=window%:c%!4=icon%
 3580 SYS "Wimp_GetIconState",,c%
 3590 PROCunstack(c%)
 3600 =c%!28
 3610 :
 3620 DEFFNicon_string(window%,icon%)
 3630 REM returns icon's indirected text string
 3640 PROCterm(FNstring_addr(window%,icon%))
 3650 =$FNstring_addr(window%,icon%)
 3660 :
 3670 DEFPROCset_icon_string(window%,icon%,a$)
 3680 REM sets icon's indirected text string
 3690 $FNstring_addr(window%,icon%)=a$
 3700 ENDPROC
 3710 :
 3720 DEFFNindirect(a$)
 3730 REM puts a$ into indirected icon workspace and returns its address
 3740 LOCAL start%
 3750 start%=ws%
 3760 IF (start%+LENa$+1)>wsend% ERROR (1<<30)+2,"Out of workspace"
 3770 $start%=a$
 3780 ws%=start%+LENa$+1
 3790 =start%
 3800 :
 3810 DEFPROCupdate_titlebar(window%)
 3820 REM redraws title bar of a window;
 3830 REM useful when adding or removing " *" to indicate unsaved data
 3840 LOCAL c%,tbbottom%
 3850 c%=FNstack(36)
 3860 !c%=window%:SYS "Wimp_GetWindowState",,c%
 3870 tbbottom%=c%!16
 3880 SYS "Wimp_GetWindowOutline",,c%
 3890 SYS "Wimp_ForceRedraw",-1,c%!4,tbbottom%,c%!12,c%!16
 3900 PROCunstack(c%)
 3910 ENDPROC
 3920 :

previousmain indexnext

 
© Martyn & Christine Fox 2004