[prev] [up] [next]

Information for Application Programmers

Contents

Introduction

This document summarizes hints for application programmers. It may not be complete and will be filled with more information as time goes by.

Portability Issues

The following chapter discusses portability issues to consider when your Smalltalk application is to be executed under different operating systems (i.e. any of UNIX, Windows or OpenVMS).

Filenames

Be careful to avoid using operating system specific file- or directory names. For example, do not assume that the directory separator in a pathName is '/' (as under Unix) or '\' (as under msdos). Do not even assume, that a simple separator-scheme is used (under OpenVMS, a completely different naming scheme is used).

For your application to be portable between operating systems, use instances of the Filename class, which hides such OS-specific knowledge and "knows" about these things.
To access a file via a path name, use "aFilename construct:relativePath" or "aFilename / relativePath" to append path components. For example, to open a file in some subdirectory, always use:

    ('dirName' asFilename construct:'fileName') readStream
or:
    ('dirName' asFilename / 'fileName') readStream
instead of:
    'dirName/fileName' readStream
or:
    'dirName\fileName' readStream
Although some automatic detection of the above situations is built into the system (which means that the above code might in fact work on some systems), the first version is guaranteed to work on all systems; even under openVMS, where the actual pathName would be "[.dirname]fileName". Of course, this is also true for whatever fancy operating system will be popular in the future.)

Always use filename-instances for file queries or file operations; do not directly invoke corresponding methods of the OperatingSystem class. For example, to delete a file, always use:

    someString asFilename delete
instead of:
    OperatingSystem deleteFile:someString

OS Commands

Be careful when OS commands are used via "OperatingSystem executeCommand: or by reading from a pipeStream via PipeStream readingFrom:.

Although the particular command may be available on other systems, the command syntax may be different.
UNIX programmers should not use any shell features, such as prefixing the command with a "cd ..." (chdir) or executing multiple commands separated by a semicolon.

If you have to use an external command, check for the type of operating system and provide alternative code (or at least a clean warning- or error message) if the system is not what you unexpected.
To check for the type of OS, use any of the isXXXlike methods, as provided by the OperatingSystem class.

For example, to get a directory listing, you could use code like:

    OperatingSystem isUNIXlike ifTrue:[
      cmd := 'ls -l'
    ] ifFalse:[
      OperatingSystem isMSDOSlike ifTrue:[
	cmd := 'dir'
      ] ifFalse:[
	OperatingSystem isVMSlike ifTrue:[
	  cmd := 'dir/full'
	] ifFalse:[
	  cmd := Dialog request:'Please enter OS command for ls'
	]
      ]
    ].
    s := PipeStream readingFrom:cmd
the above is of course a stupid example - that kind of information is easier (and portable) extracted by using protocol from the fileName class.

As a general rule, only use OS commands when absolutely required - if possible with reasonable effort, it is always better to write a corresponding piece of Smalltalk code.

If you cannot avoid the use external commands, remember the command's name and arguments in some (class-)variable and allow their configuration via some dialog (as was done for the C-compiler command inside ST/X, which is required to compile inline C-code in the browser). When done this way, a user can change the command in the private.rc startup file.

Preparing for national language variants

Although no programmer likes it, most applications are sooner or later to be provided in multiple language variants.
Smalltalk/X offers mechanisms to make national language variants possible without requiring much initial effort by the programmer and allowing for additional languages to be added later (even by the end-user or distributor, without access to the source code).

For this, ST/X provides a semiautomatic resource management, which translates strings upon request. (actually, any value can be provided by this mechanism - it is not limited to strings).

This translation is done by a class called ResourcePack, which is a subclass of Dictionary; thereby providing keyed access messages. This class knows how to read resource files - especially, it knows how to deal with conditional expressions embedded in those files, and it keeps track of language changes, caching and buffering of commonly used translations.

Programmers Interface

An instance of ResourcePack can be created manually by:
    ResourcePack fromFile:aFileName
However, it is rarely required to do this, since all classes derived from View and ApplicationModel automatically read their respective resource file and provide a resourcePack for later use.
All subclasses of the above provide a class instance variable named ClassResources (which is lazily initialized when the first instance is created), they offer a getter named classResources, and their instances have an instance variable named resources (which is initialized to the value of the class variable at instance creation time).

Thus, any method of those classes (especially: class methods) can fetch a strings translation by:

    ClassResources string:someString
while instance methods would use:
    resources string:someString
Finally, it is possible from code outside the class to translate a string using another class's resource with:
    someOtherClass classResources string:someString
If the resourcePack cannot find a translation for the argument, the argument is returned unchanged.
This has the nice effect, that a programmer can provide her native language string as key, and not care for the resourceFiles being present at first. Translations can be added at any later time, simply by editing the corresponding resource file.

To summarize:
To be prepared for later nationalization, instead of:

    Button label:'foo'
always write:
    Button label:(resources string:'foo')
Notice:
Although understood by resourcePacks, do not use the #at: message. The reason is that using #string: makes it much easier to later find all resourcePack accesses with the browser's senders function. (Beside that, the effect is the same).

Use Patterns in Resource Strings

Often, you have to embed some variable string into the translations; for example, a warnbox to tell that some file is not present may want to embed the file's name. This is done by using a variant of the above, using a string with "%i" as placeholders (i being 1 .. 9).
The above warnbox is created with:
    self
	warn:(resources
		string:'cannot find file %1'
		  with:fileName)
Always use this construct, as the placement of the filename within the text may be different in other languages (i.e. a simple concatenation of the filename will not always do the job and lead to very ugly translations).
For example, in German, the above message becomes:
    "kann die Datei %1 nicht öffnen"
as you see, the file's name is in the middle of the sentence. If your program would be "(resource string:'Cannot find '),aFilename", there would be no way to make a reasonable German sentence from it.

The translated string may contain multiple placeholders; for example:

    self
	warn:(resources
		string:'cannot find file %1 in %2'
		  with:fileName
		  with:dirName)
Variants of the "#string:with:" message are provided for up to 4 placeholders. For more placeholders, use "#string:withArgs:", passing a collection of replacement strings.

Resource File Format

Resource File Location

All resource files are to be found in the subdirectory named "resources" along the searchPath. This allows for private resource files to override the defaults, in case you don't like those translations.

Resourcefiles are named after the class, which uses those resources; thus, all resources for the WarnBox class are found in the file "resources/WarnBox.rs".
Since the ResourcePack class merges a files translations with the superclasses resources, common definitions do not have to be repeated. (however, you can if you want to override string translations in a subclass).

Resource File Encoding

Resourcefiles are pure ascii/iso8859 or utf8 text files - they can be edited with any text editor.
However, be aware that the files as provided contain 8-bit national characters and some text editors (vi on some old systems) are not 8-bit clean, corrupting the file by stripping off the 8th bit (this was true in the early 90s - now, you can savely assume, that your editor is 8-bit aware).

Also be aware, that the resource files may come along in different character encodings. Some are ISO8859-x, but most are UTF8 encoded. A comment line near the beginning of the file will declare the encoding.

ST/X's fileBrowser is save w.r.t. 8-bit characters and character encodings.

Resource File Contents

Lines in the resource file are either comments, control constructs (conditions) or translations.

Empty lines and lines starting with "#" are ignored.

Conditional constructs are of the form:

    #if someCondition
    ...
    #endif
or:
    #if someCondition
    ...
    #else
    ...
    #endif
these conditions may be nested.
The condition someCondition is a regular Smalltalk expression; Typically, the condition is based on the language setting, but theoretically, anything can be tested here.

Translations consist of two-word entries, the first word being the key (the string written down in the program), the second gives the translated string.

Example:
The following example shows translations for multiple languages:

    # this is a comment
    #
    # lets start with german, assuming that the programm is written
    # to use the english strings as keys ...
    #

    #if Language == #german

    'foo'   'quatsch'
    'no permission for %1'  'keine Leseberechtigung für Datei %1'
    'cannot open file %1'   'die Datei %1 kann nicht geöffnet werden'
    'error %1; reason: %2'  'wegen %2 ist ein %1 Fehler aufgetreten'

    #endif

    #if Language == #french

    'foo'   'je ne sais pas q''est ce que c''est'

    'no permission for %1'  'pas de permission pour ouvrir fiche %1'
    ...

    #endif

    #if Language == #italian

    ...

    #endif

Resource Files are Useful for Other Things as Well

As mentioned above: you can put other things into resource files as well; beside string translations.
All that should possibly be enduser customizable can/should be placed into a resource file (except view style settings, which are to be placed into styleSheets, to keep the look of all views consistent).
For example, to have a buttons foreground color be customizable, program the code as:
    ...
    myButton foregroundColor:(resources at:'myButtonColor' default:Color black).
    ...
and provide an (optional) entry in the resourcefile:
    ...
    myButtonColor   Color red:50 green:50 blue:100
    ...
other useful customizable things are (icon-)bitmaps, verbose levels, debugging options, default values etc.

Providing Active Help Popups (Tooltips)

As provided, ST/X provides a mechanism for automatic help popups (also called active help, to showup whenever a view is entered and no user activity occurred for some time.
(since this may be annoying for the experienced user, this is disabled by default).

The programmer interface to this is very easy: either the view or the model (applicationModel) has to provide a method called: helpTextFor: which gets the component (button, label, subview) as argument.
This method should return a short help string (obviously, this one should be translated via a resourcePack). If no such method is implemented, or the method returns nil, the helpManager displays nothing.

For example, the complete code to provide popup help in the FileBrowser is:

    helpTextFor:aComponent
	aComponent == subView ifTrue:[
	    ^ resources string:'HELP_SUBVIEW'
	].
	aComponent == fileListView ifTrue:[
	    ^ resources string:'HELP_FILELIST'
	].
	aComponent == filterField ifTrue:[
	    ^ resources string:'HELP_FILTER'
	].
	aComponent == labelView ifTrue:[
	    ^ resources string:'HELP_PATHFIELD'
	].
	^ nil
be aware, that the popup help is (currently) provided as a mechanism for application programmers; less to provide useful help for the ST/X system itself. You will find that only a few components provide any help messages (launcher, changeBrowser and fileBrowser).

Providing User Feedback (Activity Notification)

When performing long time computations or file activity, it is useful to give the user some visual feedback on what is going on.

Where to Show Feedback

Typically, this feedback is to be shown either in the active view's label (title) area or in some extra notification subview.
Lets start with an example how to show this notification in the window label (or window title):

In the label:

    |top savedLabel|

    top := StandardSystemView new.
    top extent:300 @ 300.
    top openAndWait.

    Delay waitForSeconds:1.

    savedLabel := top label.
    top label:'active ...'.
    Delay waitForSeconds:5.
    top label:savedLabel.
you may have to make certain that the label is restored, in case the activity gets interrupted or aborted. (i.e. by protecting things with an unwind block):
    |top savedLabel|

    top := StandardSystemView new.
    top extent:300 @ 300.
    top openAndWait.

    Delay waitForSeconds:1.

    savedLabel := top label.
    top label:'active ...'.
    [
	Delay waitForSeconds:5.
    ] valueNowOrOnUnwindDo:[
	top label:savedLabel.
    ]
Additionally, busy cursors are very helpful for feedBack: (the #withCursor:do: method already sets up appropriate unwind actions)
    |top savedLabel|

    top := StandardSystemView new.
    top extent:300 @ 300.
    top open.

    top waitUntilVisible.
    Delay waitForSeconds:1.

    top withCursor:(Cursor wait) do:[
	savedLabel := top label.
	top label:'active ...'. top flush.
	Delay waitForSeconds:5.
	top label:savedLabel.
    ]
More sophisticated applications may want to create an extra area for notifications and show these messages there:
    |top notifier savedLabel|

    top := StandardSystemView new.
    top extent:300 @ 300.

    notifier := Label origin:0.0@1.0 corner:1.0@1.0 in:top.
    notifier topInset:(notifier preferredExtent y + (View viewSpacing // 2)) negated.
    notifier bottomInset:View viewSpacing // 2.
    notifier horizontalInset:(View viewSpacing // 2).
    notifier level:-1; adjust:#left.

    top openAndWait.

    Delay waitForSeconds:1.

    top withCursor:(Cursor wait) do:[
	notifier label:'busy ...'.
	Delay waitForSeconds:5.
    ].
    notifier label:''.

Passing Feedback Text Around

Often, the busy activity is performed in some method which is totally unrelated to your view's application. For example, the model may perform some compute or I/O intensive operations, and is not connected to the view (i.e. it does not know where any activity notifications should be shown).
To handle this, ST/X provides an elegant mechanism based upon the the exception mechanism.

For the programmer, the mechanism is easy to use:
At any place in your code (and in any classes method), you can add statements like:

    self activityNotification:'some text'
The implementation of #activityNotification: (in Object) looks if the activityNotificationSignal is currently handled - and, if this is the case, raises the signal, passing the message string as argument. If no handler exists for it, the notification is ignored.

Every windowGroups event dispatch loop includes a handler for that signal, and sends #showActivity: to the topView. This topView should be either an instance of a user defined subclass of StandardSystemView, or have an applicationModel attached to it.
In the first case, you should redefine this method in your view class and show the message wherever appropriate. In the second case, the standard topView will forward the #showActivity message to your applicationModel, in which you should provide an appropriate method for it.

Summary:

Accessing Command Line Arguments

The command line and arguments are available in a class variable of the Smalltalk class, which can be accessed via the class message:
    Smalltalk commandLine
    Smalltalk commandName
    Smalltalk commandLineArguments

For example, if you started ST/X with:

    myApp -i myImage.img -f myScript -q foo bar baz
the returned values will be:
    Smalltalk commandLine
		-> 'myApp -f myStartupScript -q foo bar baz'

    Smalltalk commandName
		-> 'myApp'

    Smalltalk commandLineArguments
		-> 'foo bar baz'
Notice, that the VM arguments ("-i myImage.img") are never visible to Smalltalk classes, and that the startup arguments ("-f myScript -q") have been removed from the commandLineArguments return value.

Hints & Tricks

The following section gives some hints and tricks, useful for view-programming. It is definitely incomplete and will grow over time.

Asynchronous Update Processing

If you programmed your view to respond to #update message with a redraw operation, you should keep in mind, that this processing takes place in the process which originally generated the #change message.
If this is some other application or view, the processing is done NOT in the updated view.
For example, consider a browser changing some class. This results in a Smalltalk changed message to be sent, leading to #update messages to all other browsers. If the other browsers perform a redraw in their update method, the redrawing will be done by the original browsers process.

This is somewhat unfair by the others - especially, if the changing process runs at a higher priority than the updating one.
For update operations which may take some time, it should be avoided, by doing an asynchronous update.

This is done by renaming the original update method into (say) delayedUpdate and adding an update method which enters a synthetic event into the event queue, which invokes the delayedUpdate handler when the view's process regains control. i.e., the following code will do this:

    update:something with:aParameter from:changedObject
	|mySensor|

	mySensor := self sensor.        "/ holds the event queue

	"/ if an update arrives early
	mySensor isNil ifTrue:[
	    "/ mhmh - an update, but I am not yet visible (have no sensor, yet)

	    ^ self delayedUpdate:something with:someArgument from:changedObject
	].

	"/ otherwise, enter it into the sensor's event queue
	mySensor
	    pushUserEvent:#delayedUpdate:with:from:
	    for:self
	    withArguments:(Array
			     with:something
			     with:aParameter
			     with:changedObject)
    !

    delayedUpdate:something with:aParameter from:changedObject
	...
	redraw as usual
	...
    !
Now, a synthetic event is enqueued into the sensor's event queue by the update method, which, when handled will send the #delayedUpdate:with:from: message to the receiver.
The event handling will be done by the view's windowGroup process itself, whenever the scheduler resumes that process (this depends upon process priorities and system activity).

This mechanism is used in the browser - for details, see the methods in the BrowserView class.


Copyright © 1995 Claus Gittinger, all rights reserved

<cg at exept.de>

Doc $Revision: 1.33 $ $Date: 2021/03/13 18:24:51 $