Choices: correctlyChoices: path and Choices$Write variables are
one of the worst used parts of the current boot system. Not because nobody
uses it, but because very few of those people who do use it, use it
correctly.
Let's start with some fundamentals :
Choices$Path
You may not write to files in with the path Choices:*.
Choices$Write
You may not read from files in the path
<Choices$Write>.*.
So, we have one variable to read from and one to write to. Why ?
Very simple really. This way it is possible to have 'global' defaults
which are set for all users of a system, and personal defaults used by each
user. Ideally, Choices$Path is set to point first to the user's
Choices directory, followed by the global one, and Choices$Write
is set to the user's Choices directory.
Ok, now a simple example :
We have a program !DoNowt, which wishes to store its configuration details - a few switches - as a binary file.
When it starts, !DoNowt will do the equivilent of :
i%=OPENIN("Choices:DoNowt") :REM Open the file
IF i%=0 THEN
REM The file does not exist - use defaults
choices=default_value
ELSE
REM The file exists - use those in the file
choices=BGET#i%
CLOSE#i%
ENDIF
To read its configuration. But of course that isn't backward compatible with non-RPC boot structures. So, how do we get around that ? Like this :
SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read IF read<0 THENfile$="Choices:DoNowt" ELSEfile$="<DoNowt$Dir>.Choices" i%=OPENIN(file$) etc...
Which will allow it to write to its own directory if it runs on a non-RPC Boot structured machine.
But what about writing ? That is similarly easy, and we can just steal the same code pretty much :
SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read IF read<0 THENfile$="<Choices$Write>.DoNowt" ELSEfile$="<DoNowt$Dir>.Choices" o%=OPENOUT(file$) IF o%=0 THEN REM The file couldn't be opened - directory/disc full ? ERROR 0,"Could not write choices file, dying horribly" ELSE REM The file was opened - write the data BPUT#o%,choices CLOSE#o% ENDIF
Which is vaguely entertaining. Obviously the error should be reported differently, and should at worst simply do nothing as if the options had been written correctly.
Now consider that someone has upgraded their machine from non-RPC style boot sequence to one that is. This presents a problem. We have to auto-upgrade. But we can do that whilst reading the files :
SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
IF read<0 THEN
REM The variable exists - check for auto-upgrade
file$="Choices:DoNowt"
SYS "XOS_File",5,"<DoNowt$Dir>.Choices" TO ;flags
IF (flags AND 1)=0 THEN
REM The file exists, so we should move it to the new structure
SYS "XOS_FSControl",26,"<DoNowt$Dir>.Choices","<Choices$Write>.DoNowt",
(1<<7)+(1<<1) TO ;flags
IF (flags AND 1) THENERROR 0,"Could not auto-upgrade the choices file"
ENDIF
ELSE
file$="<DoNowt$Dir>.Choices"
ENDIF
i%=OPENIN(file$)
etc...
This will move the file from the application directory to the users
choices directory. This will be in the Choices: path and will
therefore be read next. Notice that we don't copy to Choices: as
this may have multiple entries. This technique requires that your application
is distributed without a choices file. If you wish to include a default
choices file you should give this another name within the application and if
the users choices file did not exist read that :
i%=OPENIN(file$)
IF i%=0 THENi%=OPENIN("<DoNowt$Dir>.Defaults")
IF i%=0 THEN
REM The defaults choices file does not exist either - generate defaults
choices=default_value
ELSE
REM The file exists - use those in the file
choices=BGET#i%
CLOSE#i%
ENDIF
The 'generate defaults' section can easily be omited if you feel that the user should never touch the inside of your application.
So, what do we have ? Well, we have an application which will load its options from the Choices system if a RPC boot sequence is installed, or its own directory if not. It will write back to the correct place for the user configuration. It is NOT up to the application to provide multi-user support except in exceptional situations - this is the point of Choices.
Ok, so lets see the code we have so far. When reading a file :
SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
IF read<0 THEN
REM The variable exists - check for auto-upgrade
file$="Choices:DoNowt"
SYS "XOS_File",5,"<DoNowt$Dir>.Choices" TO ;flags
IF (flags AND 1)=0 THEN
REM The file exists, so we should move it to the new structure
SYS "XOS_FSControl",26,"<DoNowt$Dir>.Choices","<Choices$Write>.DoNowt",
(1<<7)+(1<<1) TO ;flags
IF (flags AND 1) THENERROR 0,"Could not auto-upgrade the choices file"
ENDIF
ELSE
file$="<DoNowt$Dir>.Choices"
ENDIF
i%=OPENIN(file$)
IF i%=0 THENi%=OPENIN("<DoNowt$Dir>.Defaults")
IF i%=0 THEN
REM The defaults choices file does not exist either - generate defaults
choices=default_value
ELSE
REM The file exists - use those in the file
choices=BGET#i%
CLOSE#i%
ENDIF
When writing a file :
SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read IF read<0 THENfile$="<Choices$Write>.DoNowt" ELSEfile$="<DoNowt$Dir>.Choices" o%=OPENOUT(file$) IF o%=0 THEN REM The file couldn't be opened - directory/disc full ? ERROR 0,"Could not write choices file, dying horribly" ELSE REM The file was opened - write the data BPUT#o%,choices CLOSE#o% ENDIF
This is all well and good, but it doesn't cope with the times when we need to store many files in the Choices structure - say the choices from different areas of the program. This means we have to think a little more. The best way to sort this out is to have a function that returns the filename we should use for a particular file.
DEFFNchoicesfile(app$,name$,write)
LOCAL read,file$,flags
SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
IF read<0 THEN
REM The variable exists
IF write THEN
REM We're writing to the file
file$="<Choices$Write>."+app$+"."+name$
REM Ensure the direcotry exists
SYS "XOS_File",8,"<Choices$Write>."+app$,,,0
ELSE
REM We're reading the file
file$="Choices:"+app$+"."+name$
ENDIF
REM Check for an auto-upgrade
SYS "XOS_File",5,"<"+app$+"$Dir>.Choices."+name$ TO ;flags
IF (flags AND 1)=0 THEN
REM The file exists, so we should move it to the new structure...
IF write THEN
REM If we're writing the file, then the one we would copy is out of date
REM so we delete it
SYS "XOS_File",6,"<"+apps$+"$Dir>.Choices."+name$
ELSE
REM We are reading, upgrade the files - need to create the directory first
SYS "XOS_File",8,"<Choices$Write>."+app$,,,0
REM Now we move the file
SYS "XOS_FSControl",26,"<"+apps$+"$Dir>.Choices."+name$,
"<Choices$Write>."+app$+"."+name$,
(1<<7)+(1<<1) TO ;flags
IF (flags AND 1) THEN
REM Could not auto-upgrade the choices file - delete it!
ERROR 0,"Could not auto-upgrade the choices file"
ENDIF
ENDIF
ENDIF
ELSE
file$="<"+app$+"$Dir>.Choices."+name$
ENDIF
=file$
Notice that this deals with almost every eventuality. If there is an error during the auto-upgrade then an error will be generated. I'm sure you can think of a suitable fallback method in this situation. The easiest thing to do is just delete the original.
Remember that the function above only finds the name of the file containing our options. However, it does simplify our code a little. For reading :
file$=FNchoicesfile("DoNowt","Choices",FALSE):REM Note: FALSE
i%=OPENIN(file$)
IF i%=0 THEN
REM We just want to know the defaults
choices=default_value
ELSE
REM The file exists - use those in the file
choices=BGET#i%
CLOSE#i%
ENDIF
This removes the option of the defaults file. This can easily be added as it was above.
For writing :
file$=FNchoicesfile("DoNowt","Choices",TRUE):REM Note: TRUE
o%=OPENOUT(file$)
IF o%=0 THEN
REM The file couldn't be opened - directory/disc full ?
ERROR 0,"Could not write choices file, dying horribly"
ELSE
REM The file was opened - write the data
BPUT#o%,choices
CLOSE#o%
ENDIF
If I've written that all correctly then you should be able to write an application very easily which will not only work on RISC OS 3.1 through to RISC OS 3.7 in both networked and stand-alone environments, but also will handle the user upgrading their machine completely transparently, and running from a ReadOnly media such as CD-ROM or on a network mounted filestore.
In the example above I have dealt with a single application storing
its options. You may feel that it would be reasonable to store files for
all your applications in one place. At least one author is currently doing
this, and it reduces the problem of the 77 file per directory limit. 'New'
FileCore will be available in RISC OS 4, but it is likely that it will not be
available for older computers.
Choices:<author>.<application>.<file> may seem
a little longwinded, but it helps to reduce the number of objects in the root
of Choices:. In this case I recommend that you use your initial
and surname, eg JFletcher.
A better alternative for some programs may be to use a category directory. Acorn themselves have started the ball rolling on this with a 'WWW' directory for all Web related resources. Again, this will only work if people use it. To my knowledge only three category directories are in use at present :
It is not necessary to create a new category if the need is specialised and there are not likely to be more applications making use of it. If you feel there is a need for a new category you may wish to contact Acorn or other developers to confirm or deny any need.
When you register an application name (and have had it confirmed!), you
also have an allocation in Choices: of the same name. Bare this
in mind when creating your application.
Now the only question that remains is what to store in your configuration files. Obviously the configuration details, but I would suggest that you store them in textual format in colon delimited field format (ie like MessageTrans files). Why would you want to do this ? Well, it makes it easier on you to see what is going on in the configuration system, and for the user to modify if they need to. It is, of course, your decision as to how you store your configuration details :-)
As a word of warning, no application should ever crash because of a poorly formated configuration file. Loading an entire file into a structure (in C) is likely to cause problems if the file is corrupt in any way. Reading textual strings in BASIC should be checked for overly long strings. Non-existant tags (in tagged files) should be given default values. I'd recommend that you simply ignore any 'broken' configuration file (possibly telling the user) and use the default choices.
In general, you should not write choices unless the user asks you to. This is not practical in some situations, particularly where the choices require a reload to take effect or where there are so few that it is not sensible to include a 'Save' option.
In particular you should not write choices when your application quits. Doing so may cause invalid choices to be written, or (in the case of defaulted choices) overwrite the choices. The latter case should be avoided, as the user may have been able to 'fix' the problem (or have caused it in the first place!).