User Tools

Site Tools


pcb:preferences_subsystem

This is an old revision of the document!


Pre-existing systems: Presently there are a couple. GTK has one, there’s the one that loads command line arguments, and there’s also the menu system, which could be considered a preferences system.

I’d like to centralize this system so that other parts of the code can take advantage of it.

Requirements

  • The system should be able to interact with specified preferences files.
    This allows subsystems to create their own if they want. It will also allow the user to specify such a file at startup.
  • The system should manage the reading and writing of the files transparently.
    We don’t want developers to have to think too hard about how this works.
  • The system should not put constraints on what can be contained by the preference.
    It should be allowed to be anything. Ultimately it’s a string when it’s stored and read, but it should be convertible into any data type.
  • The files should be human readable.
    There’s an interesting question here about locales…
  • The system should be partitionable such that a subsystem may keep track of their own preferences without having to interact with the preferences of any other subsystem.

Implementation Overview

Preferences will be stored in a file as key/value pairs. An algorithm will read the file into a dictionary type structure, and then for every key it will look up that key in a “preferences registry”. The preferences registry will be a list of structures that include things like the name of the preference, some help text, default value, etc. and a pointer to a function that can be used to interpret the value string associated with the key. The function does its thing, and sets the destination variable with its value.

To save preferences this works in just the reverse. There is also a “writer” function associated with preferences in the registry, and this is called to convert the data back into a string. We generate a series of key/value pairs that are then written to a text file.

The key/value pair structures can be returned to the calling function to be retained for future inquiry. For example, maybe a plug-in chooses to store a preference in the main pcb preferences file. When pcb is initialized, if the plug-in isn't loaded yet, the preference will still be read and retained so that when the plug-in does load, the read value will be available to it for conversion.

A python pseudo-code might look something like this:

def load_preferences(filename, registry):
  file_data = read_pref_file(filename)
  for key, value in file_data:
    if key in registry:
      registry[key].reader(key, value, registry[key].ptr)
 
  return file_data
 
def save_preferences(filename, registry):
  file_data = []
  for key in registry:
    file_data.append([key, registry[key].writer(key, registry[key].ptr)])
 
  write_pref_file(file_data)
  return len(file_data)

(why is it always so much easier in python?)

Data Structures

We're going to draw heavily on the work that's already been done with the preferences (HIDAttributes) in designing the data structures for this system. The first data structure is what's populated when a preference file is read:

typedef struct
{
  /*! key */
  char * key;
  /*! value */
  char * value;
} PreferenceFileItem;

Data will be read from the file, and populate an array (object_list?) of these objects. This array will then be looped over looking for matching keys in a second, sorted, array structure containing the next data structure. The second data structure describes a preference item.

/*! 
 * \brief PreferenceItem data structure
 */
typedef struct
{
  /*! key that identifies the preference */
  char * key; 
  /*! Human readable name of the preference */
  char * name;
  /*! Text that describes what the preference influences */
  char * help_text;
  /*! Reader function that converts the value string into useful data */
  void (*reader)(char * index_str, char * input_str, void * ptr);
  /*! Writer function that converts the value back into a string */
  void (*writer)(char * index_str, void * ptr);
  /*! data pointer passed to reader and writer */
  void * ptr;
  /*! a string that can be used to initialize the preference */
  char * default_str;
} PreferenceItem;

There will be a number of “reader”/“writer” functions implemented by default to handle common data types like floats, ints, coords, etc. The data pointer will be passed to the reader and writer functions, and could contain anything. But it probably is a pointer to the variable to be set by the converted value from the PreferenceFileItem. Using function pointers like this allows for a lot of flexibility in how subsystems can use this code. The reader functions, for example, may also be responsible for notifying a GUI that the preference has been updated.

Note: the default_str should initialize the value to something that is SAFE, i.e. something that will never, ever cause PCB to crash.

Functions

There are a number of key functions here: load_preferences, save_preferences, read_pref_file, write_pref_file, apply_preferences, collect_preferences, readers, and writers. Note that the file IO is being deliberately kept separate so that the actual file format is independent. Also, this allows data from a single file to be scanned more than once against different preference registries. We saw some pseudo-code for the load_preferences and save_preferences above (although it should have used apply_preferences and collect_preferences instead of spelling it out). So let's think about some of the others, again in pseudo-code (python…).

def read_pref_file(filename):
  with open(filename, r) as f:
    f.seek(-1) # seek to the end
    flen = f.tell()
    r.rewind()
    file_data = f.read(flen)
 
  # make sure to add a null at the end so file_data[flen] is a null char.
 
  # find all the new lines
  lines = [0]
  for i in range(flen):
    c = data[i]
    if c == "\n":
      data[i] = '\0'
      if i + 1 < flen:  # more data in file
        lines.append(i+1)
 
  keys = []
  for l in lines:
    # ignore lines that start with "#"
    if data[l] == "#": continue
    # ignore leading whitespace
    i = l
    while i < flen:
      if data[l] in [" ", "\t"]:
        i++
 
      else:
        keys.append[l]
        break
 
  if len(keys) == 0: return
 
  values = []
  for k in keys:
    i = k
    while i =< flen:
      c = data[i]
      if (c == '\0') or (i == flen-1):
        # no value for this key
        # point it at the key? At a null?
        values.append(i)
        break
 
      elif c in [" ", "\t"]:
        data[i] = '\0'
        if i + 1 < flen:
          values.append(i+1)
        break
 
      i++
 

File Format

  • The file should contain 1 preference per line.
  • First word on the line (everything up to the first white space) is the name of the preference (key).
  • Everything after the first word is the value of the preference.
  • “#” should indicate a comment and the line should be ignored.
    Should I allow hashes in the middle of a line, or should everything after a hash be a comment? Colors are often specified with hashes in front, so, that might make it awkward. For now it will only be lines starting with hashes that are treated as comments.

Using lines seems like the natural thing to do, but this precludes multi-line strings. Perhaps that’s okay? I’m going to go with that for now.

Example:

# gtk preferences
gtk-layer1-color #FF0000
# snapping preferences
snap-line-radius 42
# drc preferences
drc-linewidth-min 8 mil

The above would create a list with three key value pairs:

Key Value
gtk-layer1-color #FF0000
snap-line-radius 42
drc-linewidth-min 8 mil

Thoughts on processing:

I’ve also considered if there should be some type of hierarchical structure. Like for example, subsystems could register prefixes like “gtk-” with the system, and then the system passes them the preferences with that prefix. With the function calling system, I don’t think this is necessary. Although it is a good idea for subsystems to use such a prefix to make editing the files easier.

Thoughts on implementation:

The pointer is probably often going to be the item that should be populated with the preference value, but doesn’t have to be. It could be the general preferences structure, for example, if there’s more than one parameter that needs to be updated.

Implement several “default” handlers: pref_float, pref_integer, pref_double, pref_coord… etc that take the preference value, convert into whatever type, and then assign. Similarly several default “writers” will be needed.

Then we’ll create an object list of these preference items. It will have to be initialized early in the startup process, before processing the preference related command line arguments.

There will be a function for reading a preferences file, and a function for writing a preferences file.

void read_pref_file (char * fname) { FILE * fp; /* open the file for reading */ }

/* void pref_float (char * index_str, char * input_str, void *ptr) * This function takes an input string, converts it to a float, and assigns it to ptr. * Return type is void only because I haven’t figured out what to do with it, or to remind you * that you have to set the value with the function. */ void pref_float (index_str, input_str, ptr) {

/* this function doesn’t care about index_str */
float fval = 0, *fptr=0;
fptr = (float*) ptr; /* cast the pointer as a float */
fval = atof(input_str); /* do the ascii to float conversion*/
/* do any checking to see if it worked? */
*fptr = fval;
/* return 0; */

}

/* char * float_pref(index_str, float fval) * This function takes an input float and converts it to a string for storage in a preferences file. * The caller owns the string memory that the result is stored in. * Should the float be a pointer to a float, or the float value itself? */ char * float_pref (index_str, float fval) {

char tstr[128]; /* This ought to be big enough for anything we’re going to realistically throw at it. */
char * fstr;
int len = 0;
len = sprintf(fstr, “%f”, fval);
fstr = malloc(len+1); /* allocate a buffer that’s exactly the right size */
strncpy(fstr, tstr, len); /* copy the new string into it */
fstr[len] = ‘\0’;
return fstr;

}

Questions: If a user loads a new preferences file, does it need to notify anything that this happened? Presently, with preferences, values are updated immediately. This means that all preferences need to be such that changing them at any given moment doesn’t lead to disaster. However, this model also provides the flexibility that, if there isn’t such a preference, it can specify its own handler function which could take care of any of the necessary tasks to enact the change.

pcb/preferences_subsystem.1593386636.txt.gz · Last modified: 2020/06/28 19:23 by cparker