Chapter 45. Managing User Preferences

This chapter describes a user preferences package. The resource database stores preference settings. Applications specify Tcl variables that are initialized from the database entries. A user interface lets the user browse and change their settings.

User customization is an important part of any complex application. There are always design decisions that could go either way. A typical approach is to choose a reasonable default, but then let users change the default setting through a preferences user interface. This chapter describes a preference package that works by tying together a Tcl variable, which the application uses, and a resource specification, which the user sets. In addition, a user interface is provided so that the user need not edit the resource database directly.

App-Defaults Files

We will assume that it is sufficient to have two sources of application defaults: a per-application database and a per-user database. In addition, we will allow for some resources to be specific to color and monochrome displays. The following example initializes the preference package by reading in the per-application and per-user resource specification files. There is also an initialization of the global array pref that will be used to hold state information about the preferences package. The Pref_Init procedure is called like this:

Pref_Init $library/foo-defaults ~/.foo-defaults

We assume $library is the directory holding support files for the foo application, and that per-user defaults will be kept in ~/.foo-defaults. These are UNIX-oriented file names. When you write cross-platform Tk applications, you will find that some file names are inherently platform-specific. The platform-independent operations described in Chapter 9 are great, but they do not change the fact that user preferences may be stored in c:/webtk/userpref.txt on Windows, Hard Disk:System:Preferences:WebTk Prefs on Macintosh, and ~/.webtk on UNIX. I find it useful to have a small amount of platform-specific startup code that defines these pathnames. The preference package uses resource files that work on all platforms:

Example 45-1. Preferences initialization

proc Pref_Init { userDefaults appDefaults } {
   global pref

   set pref(uid) 0 ;# for a unique identifier for widgets
   set pref(userDefaults) $userDefaults
   set pref(appDefaults) $appDefaults
   PrefReadFile $appDefaults startup
   if [file exists $userDefaults] {
      PrefReadFile $userDefaults user
   }
}
proc PrefReadFile { basename level } {
   if [catch {option readfile $basename $level} err] {
      Status "Error in $basename: $err"
   }
   if {[string match *color* [winfo visual .]]} {
      if [file exists $basename-color] {
         if [catch {option readfile 
               $basename-color $level} err] {
            Status "Error in $basename-color: $err"
         }
      }
   } else {
      if [file exists $basename-mono] {
         if [catch {option readfile $basename-mono 
               $level} err] {
            Status "Error in $basename-mono: $err"
         }
      }
   }
}

The PrefReadFile procedure reads a resource file and then looks for another file with the suffix -color or -mono depending on the characteristics of the display. With this scheme, a UNIX user puts generic settings in ~/.foo-defaults. They put color specifications in ~/.foo-defaults-color. They put specifications for black and white displays in ~/.foo-defaults-mono. You could extend PrefReadFile to allow for per-host files as well.

Throughout this chapter we assume that the Status procedure displays messages to the user. It could be as simple as:

proc Status { s } { puts stderr $s }

Defining Preferences

This section describes the Pref_Add procedure that an application uses to define preference items. A preference item defines a relationship between a Tcl variable and a resource name. If the Tcl variable is undefined at the time Pref_Add is called, then it is set from the value for the resource. If the resource is not defined, then the variable is set to the default value.

Note

Defining Preferences

Hide simple data structures with Tcl procedures.

A default value, a label, and a more extensive help string are associated with each item, which is represented by a Tcl list of five elements. A few short routines hide the layout of the item lists and make the rest of the code read better:

Example 45-2. Adding preference items

proc PrefVar { item } { lindex $item 0 }
proc PrefRes { item } { lindex $item 1 }
proc PrefDefault { item } { lindex $item 2 }
proc PrefComment { item } { lindex $item 3 }
proc PrefHelp { item } { lindex $item 4 }

proc Pref_Add { prefs } {
   global pref
   append pref(items) $prefs " "
   foreach item $prefs {
      set varName [PrefVar $item]
      set resName [PrefRes $item]
      set value [PrefValue $varName $resName]
      if {$value == {}} {
         # Set variables that are still not set
         set default [PrefDefault $item]
         switch -regexp -- $default {
            ^CHOICE {
               PrefValueSet $varName [lindex $default 1]
            }
            ^OFF {
               PrefValueSet $varName 0
            }
            ^ON {
               PrefValueSet $varName 1
            }
            default {
               # This is a string or numeric
               PrefValueSet $varName $default
            }
         }
      }
   }
}

The procedures PrefValue and PrefValueSet are used to query and set the value of the named variable, which can be an array element or a simple variable. The upvar #0 command sets the variable in the global scope.

Example 45-3. Setting preference variables

# PrefValue returns the value of the variable if it exists,
# otherwise it returns the resource database value
proc PrefValue { varName res } {
   upvar #0 $varName var
   if [info exists var] {
      return $var
   }
   set var [option get . $res {}]
}
# PrefValueSet defines a variable in the global scope.
proc PrefValueSet { varName value } {
   upvar #0 $varName var
   set var $value
}

An important side effect of the Pref_Add call is that the variables in the preference item are defined at the global scope. It is also worth noting that PrefValue will honor any existing value for a variable, so if the variable is already set at the global scope, then neither the resource value nor the default value will be used. It is easy to change PrefValue to always set the variable if this is not the behavior you want. Here is a sample call to Pref_Add:

Example 45-4. Using the preferences package

Pref_Add {
   {win(scrollside) scrollbarSide {CHOICE left right} 
      "Scrollbar placement"
"Scrollbars can be positioned on either the left or
right side of the text and canvas widgets."}
   {win(typeinkills) typeinKills OFF 
      "Type-in kills selection"
"This setting determines whether or not the selection
is deleted when new text is typed in."}
   {win(scrollspeed) scrollSpeed 15 "Scrolling speed"
"This parameter affects the scrolling rate when a selection
is dragged off the edge of the window. Smaller numbers
scroll faster, but can consume more CPU."}
}

Any number of preference items can be specified in a call to Pref_Add. The list-of-lists structure is created by proper placement of the curly braces, and it is preserved when the argument is appended to pref(items), which is the master list of preferences. In this example, Pref_Add gets passed a single argument that is a Tcl list with three elements. The Tcl variables are array elements, presumably related to the Win module of the application. The resource names are associated with the main application as opposed to any particular widget. They are specified in the database like this:

*scrollbarSide: left
*typeinKills: 0
*scrollSpeed: 15

The Preferences User Interface

The figure shows the interface for the items added with the Pref_Add command given in the previous section. The pop-up window with the extended help text appears after you click on “Scrollbar placement.” The user interface to the preference settings is table-driven. As a result of all the Pref_Add calls, a single list of all the preference items is built. The interface is constructed by looping through this list and creating a user interface item for each:

Example 45-5. A user interface to the preference items

A user interface to the preference itemsuser interface to preferences
proc Pref_Dialog {} {
   global pref
   if [catch {toplevel .pref}] {
      raise .pref
   } else {
      wm title .pref "Preferences"
      set buttons [frame .pref.but -bd 5]
      pack .pref.but -side top -fill x
      button $buttons.quit -text Dismiss 
         -command {PrefDismiss}
      button $buttons.save -text Save 
         -command {PrefSave}
      button $buttons.reset -text Reset 
         -command {PrefReset ; PrefDismiss}
      label $buttons.label 
          -text "Click labels for info on each item"
      pack $buttons.label -side left -fill x
      pack $buttons.quit $buttons.save $buttons.reset 
         -side right -padx 4

      frame .pref.b -borderwidth 2 -relief raised
      pack .pref.b -fill both
      set body [frame .pref.b.b -bd 10]
      pack .pref.b.b -fill both

      set maxWidth 0
      foreach item $pref(items) {
         set len [string length [PrefComment $item]]
         if {$len > $maxWidth} {
            set maxWidth $len
         }
      }
      set pref(uid) 0
      foreach item $pref(items) {
         PrefDialogItem $body $item $maxWidth
      }
   }
}

The interface supports three different types of preference items: boolean, choice, and general value. A boolean is implemented with a checkbutton that is tied to the Tcl variable, which will get a value of either 0 or 1. A boolean is identified by a default value that is either ON or OFF. A choice item is implemented as a set of radiobuttons, one for each choice. A choice item is identified by a default value that is a list with the first element equal to CHOICE. The remaining list items are the choices, with the first one being the default choice. A regexp is used to check for CHOICE instead of using list operations. This is because Tcl 8.0 will complain if the value is not a proper list, which could happen with arbitrary values. If neither of these cases, boolean or choice, are detected, then an entry widget is created to hold the general value of the preference item:

Example 45-6. Interface objects for different preference types

proc PrefDialogItem { frame item width } {
   global pref
   incr pref(uid)
   set f [frame $frame.p$pref(uid) -borderwidth 2]
   pack $f -fill x
   label $f.label -text [PrefComment $item] -width $width
   bind $f.label <1> 
      [list PrefItemHelp %X %Y [PrefHelp $item]]
   pack $f.label -side left
   set default [PrefDefault $item]
   if {[regexp "^CHOICE " $default]} {
      foreach choice [lreplace $default 0 0] {
         incr pref(uid)
         radiobutton $f.c$pref(uid) -text $choice 
            -variable [PrefVar $item] -value $choice
         pack $f.c$pref(uid) -side left
      }
   } else {
      if {$default == "OFF" || $default == "ON"} {
         # This is a boolean
         set varName [PrefVar $item]
         checkbutton $f.check -variable $varName 
            -command [list PrefFixupBoolean $f.check $varName]
         PrefFixupBoolean $f.check $varName
         pack $f.check -side left
      } else {
            # This is a string or numeric
            entry $f.entry -width 10 -relief sunken
            pack $f.entry -side left -fill x -expand true
            set pref(entry,[PrefVar $item]) $f.entry
            set varName [PrefVar $item]
            $f.entry insert 0 [uplevel #0 [list set $varName]]
            bind $f.entry <Return> "PrefEntrySet %W $varName"
      }
   }
}
proc PrefFixupBoolean {check varname} {
   upvar #0 $varname var
   # Update the checkbutton text each time it changes
   if {$var} {
      $check config -text On
   } else {
      $check config -text Off
   }
}	
proc PrefEntrySet { entry varName } {
   PrefValueSet $varName [$entry get]
}
Interface objects for different preference typesdialogdata-driven approachdata-driven user interface
bind $f.entry <Return> "PrefValueSet $varName [%W get]"

The binding on <Return> is done as opposed to using the -textvariable option because it interacts with traces on the variable a bit better. With trace you can arrange for a Tcl command to be executed when a variable is changed, as in Example 45-10 on page 680. For a general preference item it is better to wait until the complete value is entered before responding to its new value.

The other aspect of the user interface is the display of additional help information for each item. If there are lots of preference items, then there isn't enough room to display this information directly. Instead, clicking on the short description for each item brings up a toplevel with the help text for that item. The toplevel is marked transient so that the window manager does not decorate it:

Example 45-7. Displaying the help text for an item

proc PrefItemHelp { x y text } {
   catch {destroy .prefitemhelp}
   if {$text == {}} {
      return
   }
   set self [toplevel .prefitemhelp -class Itemhelp]
   wm title $self "Item help" 
   wm geometry $self +[expr $x+10]+[expr $y+10]
   wm transient $self .pref
   message $self.msg -text $text -aspect 1500
   pack $self.msg
   bind $self.msg <1> {PrefNukeItemHelp .prefitemhelp}
   .pref.but.label configure -text 
      "Click on pop-up or another label"
}
proc PrefNukeItemHelp { t } {
   .pref.but.label configure -text 
      "Click labels for info on each item"
   destroy $t
}

Managing the Preferences File

The preference settings are saved in the per-user file. The file is divided into two parts. The tail is automatically rewritten by the preferences package. Users can manually add resource specifications to the beginning of the file and they will be preserved:

Example 45-8. Saving preferences settings to a file

# PrefSave writes the resource specifications to the
# end of the per-user resource file,
proc PrefSave {} {
   global pref
   if [catch {
      set old [open $pref(userDefaults) r]
      set oldValues [split [read $old] 
]
      close $old
   }] {
      set oldValues {}
   }
   if [catch {open $pref(userDefaults).new w} out] {
      .pref.but.label configure -text 
      "Cannot save in $pref(userDefaults).new: $out"
      return
   }
   foreach line $oldValues {
      if {$line == 
            "!!! Lines below here automatically added"} {
         break
      } else {
         puts $out $line
      }
   }
   puts $out "!!! Lines below here automatically added"
   puts $out "!!! [exec date]"
   puts $out "!!! Do not edit below here"
   foreach item $preferences {
      set varName [PrefVar $item]
      set resName [PrefRes $item]
      if [info exists pref(entry,$varName)] {
         PrefEntrySet $pref(entry,$varName) $varName
      }
      set value [PrefValue $varName $resName]
      puts $out [format "%s	%s" *${resName}: $value]
   }
   close $out
   set new [glob $pref(userDefaults).new]
   set old [file root $new]
   if [catch {file rename -force $new $old} err] {
      Status "Cannot install $new: $err"
      return
   }
   PrefDismiss
}

There is one fine point in PrefSave. The value from the entry widget for general-purpose items is obtained explicitly in case the user has not already pressed <Return> to update the Tcl variable.

The interface is rounded out with the PrefReset and PrefDismiss procedures. A reset is achieved by clearing the option database and reloading it, and then temporarily clearing the preference items and their associated variables and then redefining them with Pref_Add.

Example 45-9. Read settings from the preferences file

proc PrefReset {} {
   global pref
   # Re-read user defaults
   option clear
   PrefReadFile $pref(appDefaults) startup
   PrefReadFile $pref(userDefaults) user
   # Clear variables
   set items $pref(items)
   set pref(items) {}
   foreach item $items {
      uplevel #0 [list unset [PrefVar $item]]
   }
   # Restore values
   Pref_Add $items
}
proc PrefDismiss {} {
   destroy .pref
   catch {destroy .prefitemhelp}
}

Tracing Changes to Preference Variables

Suppose, for example, we want to repack the scrollbars when the user changes their scrollside setting from left to right. This is done by setting a trace on the win(scrollside) variable. When the user changes that via the user interface, the trace routine is called. The trace command and its associated procedure are shown in the next example. The variable must be declared global before setting up the trace, which is not otherwise required if Pref_Add is the only command using the variable.

Example 45-10. Tracing a Tcl variable in a preference item

Pref_Add {
   {win(scrollside) scrollbarSide {CHOICE left right} 
      "Scrollbar placement"
"Scrollbars can be positioned on either the left or
right side of the text and canvas widgets."}
}
global win
set win(lastscrollside) $win(scrollside)
trace variable win(scrollside) w ScrollFixup
# Assume win(scrollbar) identifies the scrollbar widget
proc ScrollFixup { name1 name2 op } {
   global win
   if {$win(scrollside) != $win(lastscrollside)} {
      set parent [lindex [pack info $win(scrollbar)] 1]
      pack forget $win(scrollbar)
      set firstchild [lindex [pack slaves $parent] 0]
      pack $win(scrollbar) -in $parent -before $firstchild 
         -side $win(scrollside) -fill y
      set win(lastscrollside) $win(scrollside)
   }
}

Improving the Package

One small improvement can be made to Pref_Add. If a user specifies a boolean resource manually, he or she might use “true” instead of one and “false” instead of zero. Pref_Add should check for those cases and set the boolean variable to one or zero to avoid errors when the variables are used in expressions.

The interface lets you dismiss it without saving your preference settings. This is either a feature that lets users try out settings without committing to them, or it is a bug. Fixing this requires introducing a parallel set of variables to shadow the real variables until the user hits Save, which is tedious to implement. You can also use a grab as described in Chapter 39 to prevent the user from doing anything but setting preferences.

This preference package is a slightly simplified version of one I developed for exmh, which has so many preference items that a two-level scheme is necessary. The first level is a menu of preference sections, and each section is created with a single call to Pref_Add. This requires additional arguments to Pref_Add to provide a title for the section and some overall information about the preference section. The display code changes a small amount. The code for the exmh is on the CD-ROM.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.196.184