| Edit | Rename | Changes | History | Upload | Download | Back to Top |
First draft by Vassili Bykov, January 30, 2003
This document describes the settings framework, as included in VisualWorks 7.1. In its current form, the framework manages settings--application parameters that can be viewed and changed by the user in an interactive tool. The current state of the settings can be saved to a file, and later restored from that file, often in a different Smalltalk image.
The framework is designed to simplify the procedure of defining settings. While for any given application, it is usually technically easy to build a settings (or "preferences", or "options") tool, creating and maintaining such a tool requires a significant amount of tedious work laying out user interfaces, validating setting values, etc. The settings framework automates these tedious tasks, while leaving enough flexibility to override the automation in cases that require a custom approach.
This document begins with a description of the Settings Manager--the tool that organizes and displays the settings. This is the tool that allows an end user to adjust the values of application settings. We then proceed to the developer part, which begins with a walkthrough--a series of examples illustrating the most common tasks in using the settings framework. Finally, there is an overview of the framework architecture.
The Settings Manager window consists of the three main parts. There is a tree of settings pages on the left. The currently selected page is displayed on the right. At the bottom, there is a row of buttons.
For presentation, settings are organized into pages. Settings on the same page are usually related and affect the same area of the application functionality. Furthermore, settings pages themselves are organized--or can be organized--into a tree, to group together pages that are related to each other. For example, in the current VisualWorks settings, Browser and Workspace pages both appear as children of the Tools page.
When a page is selected in the tree, it is displayed on the right hand side of the Settings Manager.
The pop-up menu of the settings page tree includes the following items:
The changes to the values of settings the user makes do not take effect until they are explicitly applied. It is not necessary to apply changes made to a page before switching to another page. As long as the Settings Manager remains open, the unapplied changes on the first page are not lost.
Pressing the OK button at the bottom of the Settings Manager applies all unapplied changes on all pages and closes the window. Pressing the Cancel button closes the window without applying the changes. Pressing the Apply button applies the changes but leaves the window open. Finally, the Help button displays help, if available, for the currently selected page and the settings it contains.
Our first example is a demonstration of what exactly is a setting, and how to add your own to an existing settings page.
Let's take a closer look at the system transcript. In any workspace, evaluate and inspect Transcript. In the inspector, we see that the system transcript is an instance of the class TextCollector. Select the 'Methods' tab in the inspector, or browse that class in the browser. In the private protocol, there are two methods: characterLimit and characterLimit:. They provide access to the parameter of the transcript that controls how many characters can be written to the transcript before it starts discarding the old output. This sounds like something that the user might want changing--in other words, a user setting. Currently, the only way to change that parameter of the system transcript is sending the characterLimit: message to Transcript by evaluating an expression in a workspace. The settings framework can turn this pair of messages into a setting the user can change using an input field in the Settings tools, and the value of which can be saved and imported into another image.
Open the VisualWorks Settings window and look at the pages in the page tree. The Tools page appears to be a reasonable place for our new setting.
In the page tree view, select the Tools page, open the right mouse button menu and select Browse. A browser opens showing several methods of the VisualWorksSettings class. You can see from the selectors that there is a method for each setting on the page, plus one method defining the page itself-- the one with the selector toolsPage. We will discuss the page definition method in detail later. For now, the most important observation to make is that the UI of this page is automatically generated from the settings associated with it. There is no windowSpec to modify to add a new setting to the page. All it takes is writing a single method that describes the setting. Here is how to do it.
Add a new method with the following body to the class side of VisualWorksSettings:
toolsTranscriptLimit
<setting: #(tools transcriptLimit)>
^(IntegerSetting on: Transcript aspect: #characterLimit)
label: 'Transcript limit'
Look at the Tools page, and you should see a new label and an input field for our setting appear at the bottom. Adding this method was all that it took. Now let's consider it in more detail.
The method has two main parts: the pragma expression which marks it as a setting definition, and the body answering the setting the method defines.
Besides marking the method as a setting definition, the pragma expression also defines the ID of the setting. The ID is an array of symbols, unique to a particular setting. In our case, the ID is #(tools transcriptLimit). When settings are saved into a file and later loaded back, the ID is used to match the saved data to an existing setting definition, hence the requirement for the ID to be unique. IDs are compared using the usual = operation for Arrays, which considers two Arrays to be equal if they have the same size and all their respective elements are equal.
Additionally, IDs are used to by settings pages to select groups of settings that belong to a page. For example, all settings on the Tools page have IDs of the form #(tools <a symbol>)--in other words, their IDs all begin with the same subsequence of symbols and only differ in the last symbol. That common prefix is the reason those settings were selected to be displayed on the Tools page. You can think of a prefix as a "directory name", identifying the group a setting belongs to, while the last element of an ID is a "file name" within the group.
This explains our choice of the ID for the Transcript Limit setting: the last element, #transcriptLimit, could in principle be any other symbol (except those already taken by other settings on the Tools page), but the first symbol should necessarily be #tools, because the Tools page displays those, and only those settings whose prefix is #(tools). How did we find out we had to use that prefix? One reason is by looking at all the other settings that appear on that page, but a more compelling one is the toolsPage method itself: even though we haven't discussed page definitions in detail yet, it is fairly easy to guess from looking at the definition that the Tools page includes all settings with the prefix #(tools).
The body of the method is supposed to return a setting model: an object that knows how to get and set the value of the setting. In our case, we create an IntegerSetting on the #characterLimit aspect of Transcript--meaning that the value of the setting will be obtained by sending to the Transcript the message characterLimit, and set by sending the message characterLimit:. Declaring our setting an IntegerSetting affects how that setting is presented in the settings tool: it is shown as an input field into which the user can type an integer value. Typing anything else is not allowed, and the framework will ensure that only valid values are stored in the Transcript object.
Finally, the label: message sent to the model defines its label. The label is a short string used by the GUI to label the widgets displaying this setting.
There is an important architectural observations to make based on this first example. The Settings Framework itself does not store the values of the settings. Storage is the responsibility of the domain object, in our case the Transcript. The framework only knows how to get and set that value. Keeping track of that access information is the primary responsibility of a setting model object.
The Transcript Limit setting we defined is the most basic definition possible. While the user can only assign integer values to that setting, any integer value is allowed. In our case, negative and small positive values do not make much sense as the transcript character limit, and should probably not be allowed. To disallow them, change the setting definition to something like:
((IntegerSetting min: 1000) on: Transcript aspect: #characterLimit)
Now, when the setting is shown in the Settings Manager, its input field will not accept entries below 1000. Similarly, an upper limit, or both a lower and an upper, can be specified by using messages max: and min:max:.
If you click the Help button, you can see that the help page that is displayed contains entries for all the settings on the current page. There is no help text for our new setting. To provide it, we send the helpText: message to the setting model. Our revised setting definition will now look something like the following:
toolsTranscriptLimit
<setting: #(tools transcriptLimit)>
^((IntegerSetting min: 1000) on: Transcript aspect: #characterLimit)
label: 'Transcript limit';
helpText: 'The maximum number of characters defines ... .'
There is one last improvement we can make. Our setting appears the last on the page. Perhaps the page would look better if instead it were inserted after the Text Size setting, so that it would come after a similar-looking entry. To find out what determines the order of settings on a page, have a look at the page first, and then on the methods defining the settings included into that page. As you can see, the settings on the page appear in the same order as their definition methods in the browser--in other words, settings are sorted using the selectors of their defining methods. (Before mentally marking this as a hack, wait until you get to maintaining groups of settings, when you often need to add a new setting at a certain place in a group of already existing ones, and you will see how convenient this scheme turns out to be).
However, there is an alternative. A setting pragma can include an additional parameter, position:, to look like the following:
<setting: #(tools transcriptLimit) position: 2>
The settings that do not have that parameter are all assigned the default position value of 0. When settings are collected by a settings page, they are fist sorted by position. Then, settings with the same position values are sorted by selector as we have seen above.
You are free to pick either approach when you create your own group of settings. You can choose to not use position parameters and have settings ordered by their selectors as do all VisualWorks settings, or you can specify explicit position values in the setting declaration pragma, or mix the two approaches.
When you add a setting to a group of already existing settings, your best bet is to follow the ordering approach used by that group. In our example of adding a Transcript Limit setting, VisualWorks Settings use the selector-based approach, and therefore to reposition our new setting, we need to change its selector accordingly, to something like tools25characterLimit.
Besides IntegerSetting, there are a number of setting types--those that represent values of a certain type stored somewhere in the system. Here are the currently supported types:
Now is a good time to make a couple of final clarifications. We said that a setting definition method answers a setting, which we also called a setting model, and yet the classes we've just listed were said to be setting types. Are they the same thing?
No, they are not. Settings we are considering are all made of two parts: a setting model and a setting type. The setting model is responsible for the data access: it knows how to get and set the setting value, and also things like the label and the help text. The setting type knows what values a setting can take.
For reading convenience, a type is also used to create a setting model. This finally explains what really happens in our examples (and why the parentheses are the way they are). In our first TranscriptLimit example, we send the on:aspect: message to IntegerSetting, which is a class of a setting type. The result is a SettingModel with an instance of IntegerSetting as its type. In the second example, we first send the min: message to IntegerSetting, which creates an instance of IntegerSetting parameterized to only allow values in a certain range. Then, the on:aspect: message is sent to the instance, and the result is a SettingModel with the IntegerType instance as the type.
The on:aspect: message is thus a message understood by setting types to create setting models of those types. Here is a full list of model creation messages:
aSettingType on: aValueModel
Creates a setting whose value is obtained by sending messages value and value: to aValueModel. The argument does not have to be a ValueHolder, any object understanding those messages will do. An interesting example is using a LiteralBindingReference to access the value stored in a shared variable.
aSettingType on: anObject aspect: aSelector
This is the kind of a setting we have already seen: the value is obtained by sending a message with aSelector as the selector to anObject, and set by sending a message with the same selector with a colon appended.
aSettingType on: anObject key: keyObject
A setting created by this expression will get its value by sending the message at: to anObject with keyObject as the argument, and will set it by sending the message at:put: with keyObject as the first argument and the new value as the second one. It can be used to make a setting from a dictionary entry or an array element.
aSettingType onUISetting: aSymbol
This creation message is provided for collaboration with the old VisualWorks UISettings class. It creates a setting on a preference model stored in the dictionary of preference models of the UISettings class.
Let's now place our transcript limit setting on a page of its own. Add the following method to the VisualWorksSettings class:
transcriptPage
<settingsPage: #(tools transcript)>
^ModularSettingsPage new
label: 'Transcript';
icon: (ListIconLibrary visualFor: #tools);
settings: (self settingsWithPrefix: #(tools transcript)) The first thing we see in the method is the settingsPage: pragma. Just like a setting definition method should be marked with a setting: pragma to be recognized as such, a setting page definition should be marked with a settingPage: pragma. The argument is the page ID. Its main purpose is defining the hierarchical relationship between the pages in the page tree. It is best illustrated by an example. Given four pages with the following IDs:
Page1 #(tools)
Page2 #(tools browser)
Page3 #(tools workspace)
Page4 #(tools foo transcript)
The settings manager will arrange the pages as follows:
Page1
Page2
Page3
Page4 In other words, if an ID of one page is the prefix of an ID of another page, the page with the shorter ID is made the parent of the other one. In the example, pages 2 through 4 were made children of page 1. Note that the ID of page 4 includes an extra element between #tools and #transcript, but since (we assume) there is no page with an ID #(tools foo), it still becomes a direct child of Page 1.
The body of the method is supposed to create and return the page this method defines. In our example, the page is an instance of ModularSettingsPage, which is the class of pages that gather a group of settings and automatically generate their UI to present those settings. The label: and icon: messages provide the page with the label and icon displayed in the page tree. The settings: message gives the page the collection of settings it should contain. As you can see, we select all those settings whose ID is #(tools transcript), meaning that any setting with an ID that has the form #(tools transcript <any symbol>) is going to be included in the page.
Incidentally, in the settings tool we can see that the page we have just defined contains no settings, since there are no settings with the prefix it looks for. To move the Transcript Limit setting from the Tools page to the new Transcript page, change its ID from the old #(tools transcriptLimit) to something like #(tools transcript characterLimit).
The page we have just defined was configured the simplest possible way: we gave it a label, an icon, and a list of settings to display. The majority of pages in the VisualWorks settings tool are as simple as that. However, sometimes a more involved page configuration is required.
Have a look at the Look and Feel page in the VisualWorks settings tool. It contains a few settings rendered as drop-down lists, and a few others rendered as groups of radio buttons. As was mentioned in the setting type discussion, both of those are representations of an EnumerationSetting. Indeed, looking at the definitions of the settings on that page, we can see that all of those settings are EnumerationSettings. Why are some of them displayed as drop-down lists and others as radio buttons?
The answer is in the page definition method. After the usual (the pragma, instantiating the page, setting the label and the icon), the settings are added to the page in a little more elaborate way than in our first example. First, we add all of the settings with the prefix #(lookAndFeel), but we do so using a different message, the one that also provides a list of the setting names present among those with the prefix we are looking for, but which we don't want added at this point. The message we use for that is addAllSettings:except:. All of the EnumerationSettings that are added at this point are presented as drop-down lists, because that is the default EnumerationSetting presentation. Next, we send a message useRadioButtonsForEnumerations. That message changes the default, so that the page will now represent all EnumerationSettings as radio buttons. Then, we add two settings previously excluded from the addAllSettings:except: operation. These two become radio buttons.
Another interesting example is the Prerequisites page, available in Store images as a child of the Store page. On that page, the component for the "blessing level at least" is disabled or enabled depending on the selection in the "when loading a prerequisite" item. To accomplish that, the modules (subapplications of a modular page responsible for individual settings) for those two related settings are created explicitly and added to the page using the addModule:, rather than the addSetting: message. Then, the page is sent a message when:valueSatisfies:enable: that establishes a dependency between the aspect used by the version module to keep its current value ("version valueHolder") and the state of enablement of the level module. Then, all other settings are added to the page as usual, taking care not to add the settings for the version and the level twice.
This concludes the series of examples provided to give you the feel of the framework and to show how to accomplish the most common tasks. In the next section, we will look at the framework in a more formal way, which will place these examples in a wider context and provide you with guidance to reading the code.
The framework consists of two loosely related parts: the settings themselves and the UI. They are loosely related because, while it is expected that most settings are will be presented to the user in the settings tool, it is possible to have settings that are not included into any of the pages of the settings tool (though they would still be saved into an external file and loaded into another image). In a similar way, it is possible to have pages in the Settings Manager that do not display settings managed by the framework. For example, the System page displays the VisualWorks home directory, which is not a setting in the framework sense.
We begin with the part that deals with the settings themselves. The following are the core classes:
GenericSettingDescriptor
SettingModel
SettingType
BooleanSetting
...other setting types listed above...
SettingsDomain
InvalidSettingValueError
InvalidStoredSettingData GenericSettingDescriptor is a setting in the most general sense: an entity that knows of some important information in the image, and that can save that information in an XML form and later restore it (possibly in another image) from that XML representation. To this end, a descriptor keeps track of its ID--as described above, used to matched saved setting data to setting objects able to interpret that data and apply it to the current image. A descriptor is an abstract class, and any concrete subclass is expected to implement two methods responsible for saving and restoring the setting state to and from XML files: addStateXmlElementsTo: and restoreFrom:.
A concrete subclass SettingModel is a kind of a setting that represents a single value of a certain type stored somewhere in the image. How the value is accessed is specified at the SettingModel creation time. A number of access methods are supported, described in the Setting Creation section above. As also described in that section, SettingModels are typically created by sending messages to SettingTypes rather than to the SettingModel class itself.
An instance of SettingType is always associated with an instance of SettingModel, and describes what values are valid for that particular setting model. Thus, in the pair of a setting model and a setting type, the model knows how to get the value it represents from the image and how to set it, while the type knows what value that is supposed to be. Since a type knows what values a setting is supposed to contain, it is also responsible for saving and restoring the setting value to and from the XML form.
Finally, since a SettingModel represents a single value in the image, it supports the ValueModel protocol for getting and setting that value. In our Transcript Limit example, we defined a SettingModel that accesses its value by sending the messages characterLimit and characterLimit: to the Transcript. That same value is also accessible by sending the messages value and value: to the SettingModel itself. However, while it is possible to pass anything as the argument of the characterLimit: message sent to the Transcript, passing anything other than an Integer in the valid range as the argument of the value: message to the SettingModel will result in the InvalidSettingValueError exception.
To sum up, settings are instances of concrete subclasses of GenericSettingDescriptor. By far, the most ubiquitous of those is SettingModel, which closely cooperates with subclasses of SettingType to implement most of the settings used by VisualWorks. However, some other subclasses of GenericSettingDescriptor can provide interesting examples of creative use of settings.
To define a setting, a method with the code required to instantiate and configure a setting is added to the class side of a subclass of SettingsDomain. The subclass used by VisualWorks for its settings is named VisualWorksSettings, and that was the class we used in the examples in the Walkthrough. A domain plays the role of a setting repository, and is used to separate groups of unrelated settings. For example, if you develop an application in VisualWorks, you probably want to be able to show the settings manager with settings only for your application, and without all of those settings you see in the VisualWorks settings tool. To achieve that, you need to define a subclass of SettingsDomain for the settings of your application, and define those settings as pragma methods in that class.
As the embodiment of the group of settings of your application, a domain can be used for three main purposes:
The structure of the Settings Manager tool is quite simple. The application model class is named SettingsManager. It is opened on a particular SettingsDomain, by either sending the message openManager to the domain, or the message open: to the SettingsManager class, with the domain as the argument. The manager then collects all pages defined in the domain, as described in the Defining a Page section, and opens a window with the tree of those pages on the left hand side. When a page is selected in the tree, it is installed as a subapplication on the right hand side of the manager. The following are the most important classes:
SettingsManagerPage
ModularSettingsPage
SequenceSettingPage SettingsManagerPage is the abstract superclass of all manager pages. It takes care of tasks such as managing the child pages of a page, storing the page label and icon, and generating the help text.
The most commonly used concrete subclass is ModularSettingsPage. It generates its UI from the list of settings provided at the creation time. How to configure its instance is rather comprehensively described in the Walkthrough.
SequenceSettingPage is used to display SequenceSettings. One page is required for each SequenceSetting, since a setting is displayed as a list of items requiring a full page.
There are a number of other SettingsManagerPage subclasses. While they are not reusable, they provide examples of using the framework in more flexible ways than considered so far.
VisualWorksMemoryPolicyPage is an example of what to do if you have a setting, but the standard ModularSettingsPage cannot present it the way you would like. This page has a custom layout, but displays standard SettingModels managed by the framework.
StoreRepositoryListPage, together with StoreRepositoryListSetting, are an example of both a custom setting (though still managed by the framework), and a custom page.
VisualWorksHomePage is the most interesting example. It is a page with a custom layout, presenting data not managed by the framework (the VisualWorks home directory is not a setting in the framework sense). However, the page reuses the standard DirectorySettingModule that comes with the framework to implement its UI, by creating a temporary Setting on the data it edits.
| Edit | Rename | Changes | History | Upload | Download | Back to Top |