Beginner's Guide to WIMP
Programming
Martyn Fox

17: Saving the RISC OS Way

As long as we hold down the mouse button, our application is unaffected by the drag operation. The Wimp takes care of moving the drag box as we move the pointer, and only calls us when the button is released.

When this happens, we get a return from Wimp_Poll with a reason code of 7, meaning 'End of Drag Operation'. Let's put another line into PROCpoll to deal with this:

  280 DEFPROCpoll
  290 REM main program Wimp polling loop
  300 SYS "Wimp_Poll",,b% TO r%
  310 CASE r% OF
  320   WHEN 1:PROCredraw(b%)
  330   WHEN 2:SYS "Wimp_OpenWindow",,b%
  340   WHEN 3:SYS "Wimp_CloseWindow",,b%
  350   WHEN 6:PROCmouseclick
  360   WHEN 7:PROCdragend
  370   WHEN 8:PROCkeypress
  380   WHEN 9:PROCmenuclick
  390   WHEN 17,18:PROCreceive
  400 ENDCASE
  410 ENDPROC
  420 :

This leads us to the procedure for setting up the start of the Save operation.

Before we can save our data, we need to know the full pathname of the file that we're going to create. To do this, we get the details of the window where we dropped our icon, and send a message to its owner.

Getting the Full Pathname

The sequence of events is as follows:
  • The Wimp informs our application that the drag operation has finished
  • We get the handle of the window where the button was released, by calling Wimp_GetPointerInfo
  • We send a DataSave message, quoting the filename, to the owner of the window, namely the Filer
  • The Filer replies with a DataSaveAck message, giving us the full pathname of the file
  • Our application saves the file
  • We send a DataLoad message to the Filer
  • The Filer replies with a DataLoadAck message

Complying with Protocol

The last two steps may seem pointless; why do we need to tell the filing system to load a file that we have just saved to it?

The reason is that the protocol is designed to allow an application to save data to another application, as well as to a filing system. Usually this is done by one application saving the data to a file called 'ScrapFile', then telling the other application to load it.

This is the purpose of the DataLoad message; the DataLoadAck message which comes back from the Filer is ignored by our application, but could be used by another application to tell us that it had successfully loaded the Scrap file.

We've already seen the contents of the data block for a DataLoad message. The DataSave message has exactly the same contents, except that the string at b%+44 contains only the leafname of the file, that is the filename itself, without the preceding directory list.

We are, in effect, saying to the filing system: "I wish to save a file to the directory contained in your window so-and-so. The filename is so-and-so, please send me the full pathname."

PROCdragend sets all this up for us:

 4160 DEFPROCdragend
 4170 SYS "Wimp_GetPointerInfo",,b%
 4180 b%!20=b%!12:REM destination window handle
 4190 b%!24=b%!16:REM destination icon handle
 4200 b%!28=b%!0:REM destination x coordinate
 4210 b%!32=b%!4:REM destination y coordinate
 4220 b%!36=FNend+4-list%:REM length of data
 4230 a$=$savestr%:REM get leafname
 4240 WHILE INSTR(a$,".")<>0
 4250   n%=INSTR(a$,".")
 4260   a$=MID$(a$,n%+1)
 4270 ENDWHILE
 4280 $(b%+44)=a$:REM leafname of file
 4290 !b%=44+((LENa$+1) DIV 4)*4:REM length of block
 4300 IF ((LENa$+1) MOD 4)<>0 !b%+=4
 4310 b%!12=0:REM your_ref for original message
 4320 b%!16=1:REM Message_DataSave
 4330 SYS "Wimp_SendMessage",18,b%,b%!20
 4340 ENDPROC
 4350 :

When this procedure is called, on return from Wimp_Poll the data block contains only the minimum and maximum coordinates of the drag box, which is not a great deal of use to us in this situation. Fortunately, unless we have incredibly fast hands, the mouse pointer will still be over the window where we released the buttons, and we can find out what we want to know by a call to Wimp_GetPointerInfo.

After this call, the data block contains:

Byte   Contents
0Mouse x
4Mouse y
8Button state
12Window handle
16Icon handle

Lines 4180 to 4210 transfer this information to the correct parts of the data block for the DataSave message.

The word at byte 36 in the message data block has to contain the estimated length of the file, so we calculate this in line 4220, by subtracting the start address of the Plot Instruction List from the address of the -1 at the end of the list, which is returned by FNend and adding on four more bytes.

Getting the Leafname

We could get the filename from the string at savestr%, where we store the text contained in the writable icon in our Save box. If, however, we have already done a load or save operation, this string will contain a full pathname, which we don't want. We need to derive the leafname from this.

... the string will contain a full pathname ...

... the string will contain a full pathname ...

Lines 4230 to 4280 contain a routine for doing this. We use Basic's INSTR statement to detect the position of the first '.' in the string, and line 4260 truncates the string, so that only the part following the '.' is left. We repeat the operation until no more '.'s remain and we're left with the leafname, which we put in b%+44.

Rounding up the Block Length

The first word of the block, at b%, contains its total length, which is 44 bytes plus the length of the string, plus 1 for the Return on the end. Unfortunately, the Wimp insists that this number is a multiple of 4, so we have to round it up if it is not. Lines 4290 and 4300 take care of this.

Finally, we put a zero into the 'your_ref' number at byte 12 and a 1, which is the message action code for DataSave, into the word at byte 16. Then we send the message, with the Filer's window handle, which we get from b%+20, in R2, so the that Wimp knows where the message is going, go back to Wimp_Poll and wait for the result. We send the message with a reason code of 18, meaning 'Recorded Delivery', because we're expecting a reply.

After you have typed this in, run it (you can find the listing as page_163) and try dragging from your Save box icon. If you drop the icon over an empty part of the screen, nothing should happen, but, if you drop it over a Filer window (or the window of any application that allows you to save files into it), the result should be an error message saying 'No such function/procedure at line 1700'.

The Filer has replied with a message action code of 3, meaning DataSaveAck, and line 1710 in PROCreceive has tried to send us to a procedure we haven't yet written. It shows that the messaging system is working.

Receiving the DataSaveAck Message

Now to the next step, which leads up to actually saving the file:

 4360 DEFPROCsave
 4370 PROCterm(b%+44)
 4380 $savestr%=$(b%+44)
 4390 ENDPROC
 4400 :

This procedure is not quite complete; we'll see why in a moment, but for now, we won't do the actual save.

We've arrived here after receiving the DataSaveAck message from the Filer, which it has sent back in reply to our DataSave message. The data block is just the same as before, except that the string at b%+44 now contains the full pathname of the file.

Once again, though, the string is followed by a zero, not a Return character, so we have to sort it out using PROCterm in line 4370.

Our application would be more professional if we were to put this full pathname in the Save box writable icon, so we could make further save operations to the same directory without dragging. Line 4380 deals with this.

For the moment, we will leave it there (listing file page_164). Try running the program again and dragging the Save icon to a directory viewer. You should see the text in the Save box change from 'ShapeFile' to the full pathname. You will have to move the caret for it to appear, though, because the icon is not redrawn automatically.

Having checked that this works, add the remaining lines to PROCsave, and the following procedure, PROCsave2:

 4360 DEFPROCsave
 4370 PROCterm(b%+44)
 4380 $savestr%=$(b%+44)
 4390 PROCsave2
 4400 b%!12=b%!8
 4410 b%!16=3:REM Message_DataLoad
 4420 SYS "Wimp_SendMessage",18,b%,b%!20
 4430 ENDPROC
 4440 :
 4450 DEFPROCsave2
 4460 n%=FNend+4
 4470 SYS "XOS_CLI","SAVE "+$savestr%+" "+STR$~list%+" "+STR$~n% TO err%;flags%
 4480 IF (flags% AND 1)<>0 !err%=1<<30:SYS "OS_GenerateError",err%
 4490 SYS "XOS_CLI","SETTYPE "+$savestr%+" 012" TO err%;flags%
 4500 IF (flags% AND 1)<>0 !err%=1<<30:SYS "OS_GenerateError",err%
 4510 SYS "Wimp_CreateMenu",,-1
 4520 ENDPROC
 4530 :

Now that we have placed the full pathname of our file safely in the string at savestr%, we can go ahead and save the file. We are using a separate procedure for this so that we can call it under different circumstances.

The Save Operation

The system for saving is exactly the same as it was in our old saving routine. We know, of course, where the data starts, at list%, and we find the end with FNend, remembering that we need to tell the Filer the address of the first byte following the block we wish to save. We do both the saving and the *SETTYPE command using 'XOS_CLI' commands, treating any errors which occur as non-fatal. Finally, line 4510 closes the Save box and menu.

Having saved the file, we return to PROCsave, to perform one more task. The Wimp protocol says that, after saving, we should send a DataLoad message to the Filer. This, as we saw earlier, is to cater for the situation where we are saving our data not to a filing system, but to another application. You can prove this works in a moment.

First, though, we should now be able to save our data to a Filer window and reload it again. You should find that the Save box filename icon is updated with the full pathname after you have saved your data once. Try it; the listing file is page_165.

Saving to Another Application

Now load up Edit and click on its icon to open a window. Load up your Shapes application as well, and either load in a file or draw a picture in its window with several lines and shapes. Drag the icon from your Save box not to a Filer window, but into the Edit window.

At this point, some strange symbols will appear in the Edit window. This is the data from your Plot Instruction List, represented as ASCII codes. Don't try to interpret them; just take it that this shows that the system works.

Now open your Save box again and look at the filename icon. You will probably find that it contains the name '<Wimp$Scrap>'. This is the pathname for saving that was sent to our application by Edit. It is also a system variable. If you press F12 and type *Show at the command line, and look at the system variables, you will find that Wimp$Scrap is set to the pathname of a file, probably 'ScrapFile', probably in your !Scrap application directory.

... it contains the name '<Wimp$Scrap>'

... it contains the name '<Wimp$Scrap>'

This explains how the data was sent from our application to Edit. When Edit received our DataSave message, it ignored our leafname and sent back a DataSaveAck message containing the pathname '<Wimp$Scrap>'. Our application sent a command to the Filer to save a file under that name, and the Filer replaced the system variable name with its value and saved the file under that name in the !Scrap directory. We then sent a DataLoad message to Edit, which loaded the <Wimp$Scrap> file, then deleted it.

The system does not work in the opposite direction, though, because our application doesn't recognise an incoming DataSave message. If you would like it to do this, it's up to you to experiment.

Using the 'OK' Box

We can now save and load a file by dragging icons to and from our application. What happens, though, if we've already loaded or saved, and so have a complete pathname in our Save box? We should be able to save again just by clicking on the 'OK' icon. To do this, we need one more line in PROCsavebox, so that the entire procedure looks like this:

 3900 DEFPROCsavebox
 3910 CASE b%!16 OF
 3920   WHEN 0:IF b%!8=1 OR b%!8=4 THEN PROCchecksave
 3930   WHEN 1:IF b%!8=16 OR b%!8=64 THEN PROCdrag(b%!12,1)
 3940 ENDCASE
 3950 ENDPROC
 3960 :

This line checks for Select or Adjust being clicked on icon zero (the 'OK' icon), and sends us off to PROCchecksave:

 4550 DEFPROCchecksave
 4560 IF INSTR($savestr%,"::")<>0 AND INSTR($savestr%,"$.")<>0 THEN
 4570   PROCsave2
 4580 ELSE
 4590   SYS "Wimp_CreateMenu",,-1
 4600   ERROR 1<<30,"To save, drag the icon to a directory display"
 4610 ENDIF
 4620 ENDPROC
 4630 :

Before we try to save a file in this manner, we need to check that we have a full pathname in $savestr%. Line 4560 performs two tests on the string using the Basic INSTR statement, the first to check for the presence of a double colon and the second to check for a '$' followed by a '.'. This is pretty foolproof, but it could possibly give trouble when saving to certain unusual filing systems. You could experiment with other ways of checking the pathname's validity.

If all is well, we call PROCsave2 to save the file. If the check decides we do not have a full pathname, lines 4590 and 4600 close the menu and Save box and produce the standard error message for this situation.

The only thing left for us to do now is to make arrangements to save the file by pressing the Return key when the Save box is open. To do this, we restore the line in PROCpoll that checks for a keypress (if it was removed earlier, when it was unnecessary), and write a short procedure to handle it:

  280 DEFPROCpoll
  290 REM main program Wimp polling loop
  300 SYS "Wimp_Poll",,b% TO r%
  310 CASE r% OF
  320   WHEN 1:PROCredraw(b%)
  330   WHEN 2:SYS "Wimp_OpenWindow",,b%
  340   WHEN 3:SYS "Wimp_CloseWindow",,b%
  350   WHEN 6:PROCmouseclick
  360   WHEN 7:PROCdragend
  370   WHEN 8:PROCkeypress
  380   WHEN 9:PROCmenuclick
  390   WHEN 17,18:PROCreceive
  400 ENDCASE
  410 ENDPROC
  420 :

4640 DEFPROCkeypress 4641 REM processes keypresses in response to Wimp_Poll reason code 8 4650 IF b%!24=13 THEN 4660 !b%=saveas% 4670 SYS "Wimp_GetWindowState",,b% 4680 IF (b%!32 AND 1<<16)<>0 THEN PROCchecksave 4700 ELSE 4710 SYS "Wimp_ProcessKey",b%!24 4720 ENDIF 4730 ENDPROC 4740 :

Note that in the listing file, page_168, a dummy PROCkeypress procedure, which was present as a place-holder, has been deleted and replaced with the version shown on this page.

The procedure is only called if a key is pressed that the Wimp cannot handle on its own. This includes the Return key when the caret is in the Save box's writable icon.

As in our earlier version of this procedure, line 4650 first checks that the key pressed is Return, and passes the call on with Wimp_ProcessKey if it is not. We then discover whether the Save box is open by calling Wimp_GetWindowState and checking bit 16 of its flags. If the Save box is open, we simply have to call PROCchecksave to do the rest.

Improving Wimp Polling

At this point, we've made the final addition in this guide to the list of reason codes in PROCpoll, so this is a good place to look at the Wimp_Poll call and see how we can improve its operation.

Although our procedure handles nine different reason codes, its list is far from complete. The entire list is as follows:

Code   Reason
0Null reason code
1Redraw window request
2Open window request
3Close window request
4Pointer leaving window
5Pointer entering window
6Mouse click
7User drag box
8Key pressed
9Menu selection
10Scroll request
11Lose caret
12Gain caret
13Poll word non-zero (RISC OS 3+ only)
17User message
18User message recorded
19User message acknowledge

Many of these reason codes are now very familiar, but some are of no use at all to our particular application and the machine wastes time calling us.

The most important reason code in this respect is code zero, the null reason code. Wimp_Poll always returns with this code if it has no real reason for calling us but it is our turn to be called. We saw an example of this at the end of Section 3. We proved that our new application was multi-tasking by adding a couple of temporary lines to make it beep once every 500 times round the Wimp_Poll loop. Nothing was happening that concerned the application at the time; there were no windows to open or close, no mouse clicks and no menus to open, yet the beeps showed that it was being called, probably many hundred times a second.

The vast majority of these calls had reason code zero. Although we removed the lines which caused the beeps, the process is still continuing. Several hundred times a second, the machine hands control to our application on a return from Wimp_Poll, the program executes the CASE ... OF structure in PROCpoll without finding a match for the reason code in any of the WHEN ... lines, checks the state of quit%, returns to the top of the loop and calls Wimp_Poll again. The time taken to do this isn't noticeable when you are just using your application, but it can slow down the machine when you have the application loaded but are doing something else. Besides, the more applications you have running which are wasting time in this way, the slower the computer as a whole will run, even if it's only by a small amount.

Masking Out Reason Codes

Fortunately, we can do something about this problem. If you look at the line in PROCpoll which calls Wimp_Poll, you will see that it reads:

    SYS "Wimp_Poll",,b% TO r%

We pass the pointer to the data block in R1 but nothing in R0. Because of the way Basic's SYS command works, we actually put zero in R0.

The Wimp_Poll call allows us to put a mask word in R0 whose bits tell the Wimp not to return control to our application for certain reason codes. Bit zero masks out reason code zero, bit 1 code 1 and so on.

We can see this happening by adding a couple of temporary lines to the program, just as we did in Section 3. Insert a line in the main section of the program, near the beginning (see the file page_170):

   10 REM >!RunImage
   20 REM (C) Martyn Fox
   30 REM shape drawing program
   40 REM based on Wimp shell program v0.01
   50 version$="0.01 (date)"
   60 ON ERROR PROCclose:REPORT:PRINT" at line ";ERL:END
   70 SYS "Wimp_Initialise",200,&4B534154,"Shapes" TO ,task%
   80 PROCinit
   90 PROCcreateicon
  100 ON ERROR IF FNerror THEN PROCclose:END
  110 T=TIME
  120 REPEAT
  130   PROCpoll
  140 UNTIL quit%
  150 PROCclose
  160 END
  170 :

It doesn't actually matter where you put this line, as long as it comes before the start of the Wimp polling loop.

Now add a line to PROCpoll, after the call to Wimp_Poll but before the CASE ... OF line:

  290 DEFPROCpoll
  300 REM main program Wimp polling loop
  310 SYS "Wimp_Poll",,b% TO r%
  320 IF TIME>=T+10 VDU7:T=TIME
  330 CASE r% OF
      etc.

The effect of this line is to produce a beep on each return from Wimp_Poll, provided 1/10 second or more has elapsed since the previous beep. This prevents beeps being produced too quickly and cancelling each other out. If you now run the application, you will get a rapid succession of beeps until you quit it.

Now let's try masking out the null reason code. Leave the two temporary lines as they are but change the line which calls Wimp_Poll to read:

  310 SYS "Wimp_Poll",1,b% TO r%

This time, we put 1 in R0 which is, of course, a 32-bit number with bit zero set to 1 and all the rest clear. By doing this, we tell the Wimp to mask out reason code zero but allow all the others through.

Now run the application again (file page_171). This time, the stream of beeps will have gone, but you'll get a beep every time something happens which affects the application, such as a window being opened or redrawn. By masking out this reason code, we've greatly reduced the number of calls to our program.

You'll notice, though, that you get a beep every time you move the pointer into or out of your window. This is because of reason codes 4 and 5. In some applications, this information may be useful. We might, for example, want to redefine the pointer shape when it entered a particular window and change it back to the default shape when it left. In this application, though, these two reason codes serve no useful purpose, so we may as well mask them out.

Let us look at which reason codes we can mask out and which ones we need to leave alone.

  • Mask bits 2, 3, 7, 9, 10, 14 to 16, 20, 21 and 25 to 31 must be zero.
  • Bits 1, 6 and 8 may be set temporarily, but calls with their reason codes will be queued. This means that the machine will wait until the application accepts them.
  • Bits 22 to 24 (not listed in the previous table) fall outside the scope of this guide, so we will leave them set to zero.

We need reason codes 1, 2, 3, 5, 7, 8, 9, 17,18 and 19. This leaves bits 0, 4, 5 and 11 to 13 which we can mask out by setting them to 1.

For experimental purposes, we can write this number in binary:

    SYS "Wimp_Poll",%11100000110001,b% TO r%

Writing the mask word in this way means that you can try out the effects of changing some of the bits in this number to mask out or let through various reason codes. If, for example, you change bit 4 to a zero, you will get a beep as the pointer leaves the window; doing the same with bit 5 produces a beep as the pointer enters the window.

When you've finished experimenting, remove the two temporary lines and tidy up the line which calls Wimp_Poll by rewriting the mask word as a hexadecimal number. The equivalent in this example is &3831.

Using Reason Code Zero

You may write a program which has a use for reason code zero. You could, for example, be carrying out some kind of continuous process, but make repeated calls to Wimp_Poll to allow other tasks to continue functioning. Alternatively, you may have some reason for allowing the screen to be redrawn before your program continues what it is doing. The Save box of our application stays on the screen until the save operation has been completed. If you get an error message, for example because you try to save without a disc in the drive, the Save box stays on the screen until you've cleared the error box.

In a different situation, you may wish to click on an icon in a dialogue box and have your application perform a suitable action only after the dialogue box has disappeared. To do this, you could set a Basic variable to TRUE as a flag, call Wimp_CloseWindow to close the dialogue box, then simply return to Wimp_Poll. In your polling loop, you would then arrange for reason code zero to lead to a procedure which checks the state of this flag variable. If it is TRUE, you then call Wimp_GetWindowState and examine bit 16 of the window flags to confirm that the dialogue box really has been closed, then do whatever you have to do.

If your program works in this way, it isn't necessary to respond to every null reason code; you simply have to modify the mask word to let through the ones you want. That is, you could mask out the null reason code until you know you will need it, between closing the dialogue box and completing the associated processing, and mask it out again once your processing has been completed.

Loose Ends

There's one final correction that we ought to make to the program for completeness. When we first added our save routine, it was necessary to choose Save from the main menu to cause the file to be saved, but later we added the save box and new save functions. However, PROCmenuclick still contains the line that calls PROCsave when we choose this menu item, and that's the wrong thing to do because PROCsave is now called when we receive a DataSaveAck message (code 2). Hence, choosing the menu item now produces an error.

The easiest solution would be to remove the line entirely, so that choosing the menu item itself no longer does anything. However, a more useful alternative would be to change the line as follows, so that choosing it is equivalent to clicking the OK button in the Save box, and providing a useful shortcut to it:

 2090 DEFPROCmenuclick
 2100 REM handles mouse clicks on menu in response to Wimp_Poll reason code 9
 2110 LOCAL c%,adj%
 2120 c%=FNstack(20)
 2130 SYS "Wimp_GetPointerInfo",,c%
 2140 adj%=(c%!8 AND 1)
 2150 SYS "Wimp_DecodeMenu",,topmenu%,b%,c%
 2160 CASE $c% OF
 2170   WHEN "Quit":quit%=TRUE
 2180   WHEN "Clear":PROCclear
 2190   WHEN "Save":PROCchecksave
 2200 ENDCASE
 2210 IF adj% PROCshowmenu(topmenu%,topx%,topy%)
 2220 PROCunstack(c%)
 2230 ENDPROC
 2240 :

The version of the listing up to this point may be found as the file page_174.

previousmain indexnext

 
© Martyn & Christine Fox 2004