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:
or:
('dirName' asFilename construct:'fileName') readStream
instead of:
('dirName' asFilename / '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.)
'dirName\fileName' readStream
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:
instead of:
someString asFilename delete
OperatingSystem deleteFile:someString
"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:
the above is of course a stupid example - that kind of
information is easier (and portable)
extracted by using protocol from the fileName class.
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
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.
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.
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.
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:
while instance methods would use:
ClassResources string:someString
Finally, it is possible from code outside the class to translate a string using another class's
resource with:
resources string:someString
If the resourcePack cannot find a translation for the argument,
the argument is returned unchanged.
someOtherClass classResources string:someString
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:
always write:
Button label:'foo'
Notice:
Button label:(resources string:'foo')
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).
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).
The translated string may contain multiple placeholders;
for example:
Variants of the "
self
warn:(resources
string:'cannot find file %1 in %2'
with:fileName
with:dirName)
#string:with:
" message are provided for
up to 4 placeholders.
For more placeholders, use "#string:withArgs:
", passing
a collection of replacement strings.
"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).
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.
Empty lines and
lines starting with "#"
are ignored.
Conditional constructs are of the form:
or:
#if someCondition
...
#endif
these conditions may be nested.
#if someCondition
...
#else
...
#endif
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
...
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.
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:
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).
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
In the label:
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.
top label:savedLabel.
Additionally, busy cursors are very helpful for feedBack:
(the
|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.
]
#withCursor:do:
method already sets up appropriate unwind actions)
More sophisticated applications may want to create an extra area
for notifications and show these messages there:
|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.
]
|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:''.
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:
The implementation of
self activityNotification:'some text'
#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:
#showActivity:
method in either your topView class
or in the applicationModel.
"self activityNotification:..."
.
Smalltalk
class,
which can be accessed via the class message:
Smalltalk commandLine
Smalltalk commandName
Smalltalk commandLineArguments
#commandLine
#commandName
#commandLineArguments
For example, if you started ST/X with:
the returned values will be:
myApp -i myImage.img -f myScript -q foo bar baz
Notice, that the VM arguments (
Smalltalk commandLine
-> 'myApp -f myStartupScript -q foo bar baz'
Smalltalk commandName
-> 'myApp'
Smalltalk commandLineArguments
-> 'foo bar baz'
"-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.
#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.
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:
Now, a synthetic event is enqueued into the sensor's event queue
by the
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
...
!
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>