This example is based on the code of QABUTIL. It shows part of the code used to implement the "Options" dialog in the Transfer pull-down menu. The code has been slightly modified to show the actual map numbers used. All the map numbers refer to maps in the PROGMAKE mapset on the SYSTEM database. This is quite a nice example as it illustrates most of the key aspects of using map_get_inputs() and pick_record():
Setting jump FALSE and resetting the field number after a validation failure
How to deal with fields that are sometimes omitted, depending on previous input
How to use the PROMPT facility, including a nice reuse of a fetch callback function to do validation.
This code is called with the global variables set as follows:
show_flags = SKIP_MISSING_FIELD|LEFT_JUSTIFY;
scrn_flags = SKIP_MISSING_FIELD|LEFT_JUSTIFY|PROMPT_AVAILABLE;
df_libtype = 5;
#define MAX_HISTORY 6
extern union WK
{
DP4 utilities often declare a single wk variable and put all the save areas need for calls to pick_record() there. It can save a lot of space. Of course it would probably be a better idea to just use a temporary area, but at the time pick_record() was originally written putting this much data on the stack would have been a very bad idea, and using a heap was not an option.
struct SAVE_HISTORY
{
short pm_number;
char pm_name[sizeof(pm_name_table.pm_name)];
datetype date;
char headtext[sizeof(db_history.headtext)];
} save_history[MAX_HISTORY+1];
} wk;
void transfer_options(void);
static void option_inner(int m,int *f);
static int pick_history(void);
boolean fetch_history(int direction);
static void show_history(int mapnr,int fieldnr);
void transfer_options()
{
struct OPTION save_option;
It is good practice to ensure that a dialog that is cancelled does not change any values, so we save the variables the dialog changes first. Althoughh not needed here the saved structure can also be used for testing if anything has been changed - this is much safer than relying on the changed variable. option.quiet = quiet;
option.doing_maps = doing_maps;
save_option = option;
if (map_get_inputs(340,1,option_inner,PREDISPLAY|CLEAR|GI_GLOBALS)!=0)
{
If the dialog is cancelled restore the saved value of options option = save_option;
}
quiet = option.quiet;
doing_maps = option.doing_maps;
}
/**/
static void option_inner(int m,int *f)
{
int fieldnr = *f;
switch (fieldnr)
{
case 1:
inpm_e(&option.option,341);
break;
case 2:
if (option.option != 2)
{
Dialogs often contain fields that are not always relevant.If a particular field is not relevant or is not used for input you should miss it out by calling inpm_omit() (to just skip it) or inpm_ban() (to skip it and provide a visual hint that it is unavailable). Never just set the field number to the next field, or return without doing anything: if you do your program will hang. inpm_ban();
break;
}
if (inpm_c(option.pm_name))
break;
This field has validation. Break if inpm_x() indicates we should not do it. MOVE(pm_name_table.pm_name,option.pm_name);
pm_name_table.wordtype = VERSIONNAME;
If the user entered an invalid value or requested prompt allow him to choose from a list. Calling our a pick_record()'s fetch callback function is a nice way of validating the input, and makes sure a following call to the pick_record() is properly initalised. We can't pass EQUAL, because that has a special meaning to the fetch function. But we can pass EQUAL|DEPTH or EQUAL|BUT. if ((!fetch_history(EQUAL|DEPTH) || abort_code) && pick_history())
{
If we get here the user was offered prompt, but cancelled without making a choice. So we need to reset the field number in this case, and must set jump FALSE jump = FALSE;
*f = fieldnr;
break;
}
MOVE(option.pm_name,pm_name_table.pm_name);
showc(m,fieldnr,option.pm_name);
The value displayed in field 2 needs to be updated because the user may have chosen the value from prompt. Even if he entered a value, it may need to be changed because the name uses type S, for which case and whitespace are not significant. option.timestamp = db_history.timestamp;
option.export_era = pm_name_table.pm_number;
break;
case 3:
if (option.option != 3)
{
inpm_ban();
break;
}
if (old_dbnr > 0 && old_dbnr != main_dbnr)
{
db_nr = old_dbnr;
db_close();
db_nr = main_dbnr;
old_dbnr = 0;
Some nasty manipulation needed to make sure we don't unintentionally leave databases open, or accidentally close the main database or the system database. }
if (inpm_unp(option.dbname))
break;
/* strip any trailing spaces off dbname */
{
register int i;
for (i = 0;option.dbname[i]!=' ' && option.dbname[i];i++)
;
option.dbname[i] = 0;
}
old_dbnr = db_open(option.dbname,0,READ_ONLY+MISSING_OK);
db_nr = main_dbnr;
if (old_dbnr < 0)
{
User specified an invalid database, so we complain and reset the field number. jump = FALSE;
map_draw(1,SYSTEMMAP|IN_BOX|REMARK|ERRORMAP);
*f = fieldnr;
break;
}
break;
case 4:
inpm_y(&option.doing_maps);
break;
case 5:
inpm_y(&option.compact_format);
break;
case 6:
inpm_y(&option.no_macros);
break;
case 7:
inpm_y(&option.quiet);
break;
case 8:
inpm_y(&option.controlled);
break;
A few easy fields there! field 9 is similar to field 2 case 9:
if (!option.controlled)
{
inpm_ban();
break;
}
if (inpm_c(option.post_pm_name))
break;
MOVE(pm_name_table.pm_name,option.post_pm_name);
pm_name_table.wordtype = VERSIONNAME;
if ((!fetch_history(EQUAL|DEPTH) || abort_code) && pick_history())
{
jump = FALSE;
*f = fieldnr;
Actually the real code of QABUTIL set the field number to 8 here, and used 8 in the call to show_c() below - it did not use the fieldnr variable, so this field did not work properly!. This bug was probably introduced when a new field was added to the map at some point. Using the saved value of the initial field number helps protect against this kind of bug, as can be seen here. break;
}
MOVE(option.post_pm_name,pm_name_table.pm_name);
show_c(m,fieldnr,option.post_pm_name);
option.control_stamp = db_history.timestamp;
option.post_era = pm_name_table.pm_number;
break;
case 10:
inpm_y(&option.ignore_errors);
break;
case 11:
inpm_y(&option.test_only);
break;
default:
inpm_omit();
break;
Calling inpm_omit() in default: is very important in your data_inner() callback function:
}
}
/**/
static int pick_history()
{
return pick_record(492,0,0,fetch_history,show_history,wk.save_history,
sizeof(wk.save_history[0]),MAX_HISTORY,DEFAULT);
}
/**/
boolean fetch_history(direction)
{
if (direction == EQUAL)
{
pick_record() passes the direction==EQUAL flag to indicate that we need to reset our position in the table. That is why the calls to fetch_history() in the options_inner() function pass EQUAL|DEPTH - they don't want to end up here. Resetting involves getting the key out our save structure and calling rec_fetch_main(EQUAL) regardless of what index we are fetching on. If we wanted to improve performance we could use the bf_() (buffered fetch) functions. db_history.internal.pm_number = wk.save_history[0].pm_number;
rec_fetch_main(EQUAL,&db_history.l);
pm_name(VERSIONNAME,db_history.internal.pm_number);
}
else
{
while (rec_fetch_main(direction|DEPTH,&pm_name_table.l,1))
{
We are putting a filter on the data here. The manipulation of direction is typical. We have to test against EQUAL because we want fetch_history(EQUAL|DEPTH) to return FALSE when there is no match. db_history.internal.pm_number = pm_name_table.pm_number;
if (rec_fetch_main(EQUAL,&db_history.l) || direction & EQUAL)
break;
if (direction & FIRST)
direction = NEXT;
if (direction & LAST)
direction = PREV;
}
if (there)
{
MOVE(wk.save_history[0].pm_name,pm_name_table.pm_name);
MOVE(wk.save_history[0].headtext,db_history.headtext);
wk.save_history[0].pm_number = db_history.internal.pm_number;
wk.save_history[0].date = db_history.date;
}
}
return there;
}
/**/
static void show_history(int mapnr,int fieldnr)
{
if (fieldnr == 1)
show_over(mapnr,101,496);
A not quite typical pick_record() show callback. The callback is ensuring some non list item related data is also displayed in the popup. This could probably have been done better another way. map_attach(mapnr,fieldnr,494,DEFAULT); show_c(494,1,wk.save_history[fieldnr].pm_name); show_h(494,2,wk.save_history[fieldnr].pm_number); show_d(494,3,&wk.save_history[fieldnr].date); show_c(494,4,wk.save_history[fieldnr].headtext); map_glue(mapnr,494); } |