This document is being prepared - it is not complete.
to be written ...
to be written ...
to be written ...
more to be written ...
Learning 'by doing' is usually much better
than 'by reading manuals'.
Therefore,
it is suggested that you best learn by creating your own little goodies,
taking existing code as an example
(i.e. copy some code which creates a view looking similar to what you need,
and modify/enhance it step by step).
The examples below can be selected and evaluated in the file browser
using
doit from the menu
or by typing
"CMD-d"
on the keyboard.
If you are looking at this text using a HTML reader (FireFox, Chrome etc.),
copy the code fragment from the viewer and
paste
it into a workspace. Then select and execute it there.
Finally, if you read it via the ST/X documentation reader,
some code fragments can be directly executed by clicking on them.
Executable code is marked with a different (dark red) color, as in:
Transcript topView raise.
Transcript flash
Read the text and the example code, try to understand what's going on, execute the example and play around with the parameters (i.e. get the code into a workspace, modify it and execute it again).
Each example provides you some new information based on the previous ones, thus together providing step by step lessions. For beginners, it does not make sense to skip examples. Read the text, and execute the corresponding examples in sequence.
Also, look into the actual code implementing the used functionality.
Do so by opening a SystemBrowser
and inspect the code.
Since the examples only cover a fraction of the full functionality,
this reading may also lead to more advanced uses of some classes.
Especially have a look at the classes' documentation and examples which are found in
the class protocol under the documentation category.
As a side effect, you will also learn how to find your way through the system using the various search functions in the browser.
Close all example views using the window manager.
In contrast, many non-smalltalk user interfaces (for example: Motif, Xtoolkit etc) are based upon a callback model of operation. Here, a widget also contains controller functionality, and often has a tight relation to the data being represented (i.e. the model). User actions (such as press of a button) are forwarded via a callback function to the application.
In Smalltalk/X, widgets can be used both with and without
a model. To provide both compatibility to existing smalltalk setups AND
beginners an easier start, both mechanisms are supported.
For simple widgets, there is usually no need to define or create
special model and controller classes;
instead, the operation of the widget can be controlled by giving it
actionBlocks which are evaluated on user interaction.
In the following, we will start by describing the non-MVC operation.
MVC operation is described in more detail in a section below.
View
.
A view can have subviews, and each of these subviews
has that view as their common superview.
Notice for the curious:
Topviews also have a special place in ST/X with respect to event handling:Let us create & show our first view:
a topView together with all of its subviews are handled by one process within ST/X and will usually be served from one shared event queue.
This means, that within such a so called windowGroup execution is normally not parallel. (However, with some tricks, you can arrange for subviews to be put into separate windowgroups.)
(View new) open "create a view, and make it show itself"
The above code performs two steps:
View new
View
-class for a new instance of itself;
View
will create one, initialize it and return it.
open
Instances of this (general) view-class do not support icons and window-labels.
There is a specialized class, called StandardSystemView
,
which was written exactly for that purpose.
Usually, all of your topviews should be instances of this class,
or of a subclass of it.
Although it is possible to use any view as a topView (as was done in the
above example and will be done later for demonstration purposes),
applications should use an instance of StandardSystemView
as the topView.
Changing the above example to use StandardSystemView
:
opens a view with a (default) label, a (default) icon and a default size.
(StandardSystemView new) open "create a view, and make it show itself"
open
message. This creates a new process and made the view
independent of the currently running process.
v openModal (where v is the view)
If opened this way, the currently executing process will not get control
until the view is closed. Modal opening is typically done for dialogs which have
to be finished before operation of the main view is to be continued.
Internally, there are two open messages which are understood by views:
openModeless
and
openModal
Each view knows what is the most useful way of opening itself,
and the general
v open
is redefined according to that.
#open
to a StandardSystemView
will open
it modeless, while sending it to some Dialogbox opens it modal).
Therefore, you usually do not have to take care of this yourself,
just use #open
for all
views (of course, there could be applications where this is not true,
there you should use
an explicit #openModal
or #openModeless
).
When a view is opened, all of its subviews (we will shortly learn more on this)
are opened with it - however, all under control of the single windowGroup
process. There is no need to open all views individually.
However, it is possible to arrange for subviews to be
initially invisible.
See more below in the "Window Modes"-chapter.
|v|
v := StandardSystemView new. "create new topview"
v label:'Hello World'. "set its window-label"
v icon:(Image fromFile:'../../goodies/bitmaps/gifImages/balloon_tiny.gif'). "set its icon"
v iconLabel:'world'. "set its iconlabel"
v open "- bring it up"
Just to see the difference, try (read on before doing it):
|v|
v := StandardSystemView new.
v label:'Hello World'.
v icon:(Image fromFile:'clients/Demos/bitmaps/hello_world.icon').
v iconLabel:'world'.
v openModal "- bring it up modal"
and find out, that the interaction with this view is lost
until the helloview is closed. However, you can still interact with other
topviews. Close the hello-view with the window manager.
Notice:
For reasons not to be explained here, the above is not exactly true for the documentation viewer; Its actions are performed in a separate process and are not affected by a modalBox being modal)
There is currently no ST/X icon editor available, but you can create and edit bitmaps using X's bitmap tools, or any other icon editor provided by your system. (ST/X supports many different image formats: XBM, XPM, SUN-ICON , Windows & OS/2's BMP formats, GIF, face and even TIFF and Targa formats).
The following demonstrates this with a nice icon:
Hint:
|v|
v := StandardSystemView new.
v label:'Hello World'.
v icon:((Image fromFile:'goodies/bitmaps/gifImages/garfield.gif') magnifiedTo:64 @ 64).
v iconLabel:'world'.
v open
You should use black&white images for icons, since some
displays do not support color images.
Althought ST/X does convert the image to
black&white, on these displays, this may not look too pretty for some images.
By the way: you can programatically force iconification and deiconification
of a topView, by sending it a #collapse
or #expand
message.
For example:
You can also force a topView to be started as icon:
|v|
v := StandardSystemView new.
v label:'Hello World'.
v icon:((Image fromFile:'goodies/bitmaps/gifImages/garfield.gif') magnifiedTo:64 @ 64).
v iconLabel:'world'.
v open.
Delay waitForSeconds:5.
v collapse.
Delay waitForSeconds:5.
v expand.
or force it to be opened at a particular position
(instead of whatever the windowManagers default is):
|v|
v := StandardSystemView new.
v label:'Hello World'.
v icon:((Image fromFile:'goodies/bitmaps/gifImages/garfield.gif') magnifiedTo:64 @ 64).
v iconLabel:'world'.
v openIconified.
Notice: it has been reported, that some window managers ignore this,
and always position the topView or always ask the user to provide a
frame for it.
|v|
v := StandardSystemView new.
v label:'Hello World'.
v icon:((Image fromFile:'goodies/bitmaps/gifImages/garfield.gif') magnifiedTo:64 @ 64).
v iconLabel:'world'.
v openAt:100@100.
defaultExtent
-method.
For StandardSystemView
, the default value is
640 @ 400
(read as: "640 pixels wide and 400 pixels high"),
100 @ 100
(which is also normally not what you want).
You can change a views extent with:
v extent:(400 @ 300)
or (better style !) in a device independent way, with:
v extent:(Display pixelPerMillimeter * (20 @ 10)) rounded
Now you see, why opening is a separate action from view creation:
you can change
all these settings before the view is visible - otherwise you would get quite some
visible action on your display! Ttry changing the extent after the open.
|v|
v := StandardSystemView new.
v label:'Hello World'.
v icon:(Image fromFile:'clients/Demos/bitmaps/hello_world.icon').
v iconLabel:'world'.
v extent:(400 @ 300). "- set its extent"
v open
a topviews extent is normally under control of the window system.
This means, that the window manager lets the user specify
the size of the view (however, some window managers show the view immediately).
|v|
v := StandardSystemView new.
v label:'try to resize me'.
v extent:(300 @ 300).
v maxExtent:(600 @ 600).
v minExtent:(200 @ 200).
v open
you already thought this: a fix size is done with:
|v|
v := StandardSystemView new.
v label:'no way to resize me'.
v extent:(300 @ 300).
v maxExtent:(300 @ 300).
v minExtent:(300 @ 300).
v open
In order to understand the following examples,
we have to make a little excursion,
and learn how to create subviews.
...
topView := StandardSystemView new.
...
subView := View in:topView.
...
topView open.
...
or created without a superview relationship at first, and later
placed into another view (the ST-80 style),
as in:
...
topView := StandardSystemView new.
...
subView := View new.
...
topView add:subView.
...
topView open.
...
the two mechanisms are almost identical
(*),
choose whichever is more convenient.
A views position is called origin and is specified much like
its extent. Be prepared, that most window managers simply ignore the given
origin for topViews - either placing it somewhere on the screen, or asking
the user to position the view by showing a ghostline.
#origin:
,
#extent:
or #corner:
message is an integer,
it is interpreted as pixels.
If its a float or fraction, its value should be between 0.0 and 1.0 and
is interpreted as that fraction of the superviews size.
|v sub1 sub2|
v := StandardSystemView new.
v extent:300 @ 300.
sub1 := Button in:v.
sub1 label:'button1'.
sub1 origin:10 @ 10.
sub1 extent:100 @ 100.
sub2 := Button in:v.
sub2 label:'button2'.
sub2 origin:10 @ 120.
sub2 extent:100 @ 100.
v open
many views offer more convenient instance creation methods,
in which the dimension and/or additional attributes can be specified.
|v sub1 sub2|
v := StandardSystemView extent:300 @ 300.
sub1 := Button label:'button1' in:v.
sub1 origin:10 @ 10 extent:100 @ 100.
sub2 := Button label:'button2' in:v.
sub2 origin:10 @ 120 extent:100 @ 100.
v open
Have a look at a views class protocol in the browser to get a feeling
of what is provided.
Hint:
You do not have to learn and use those; you can always use individual messages to get the same effect.Instead of specifying an extent, you can also specify the position of the lower-right corner point:
Especially beginners should not confuse themselves by trying to learn all of this protocol by heart in the beginning.
|v sub1 sub2|
v := StandardSystemView extent:300 @ 300.
sub1 := Button label:'button1' in:v.
sub1 origin:10 @ 10 corner:110 @ 110.
sub2 := Button label:'button2' in:v.
sub2 origin:10 @ 120 corner:110 @ 210.
v open
Due to rounding errors when pixel coordinates are computed,
it is generally better to use #origin:corner:
messages instead of
#origin:extent:
, when multiple subviews are to be placed into
some frame view.
|top sub1 sub2|
top := StandardSystemView extent:100@100.
top viewBackground:Color black.
sub1 := View in:top.
sub1 viewBackground:Color red.
sub1 origin:0 @ 0 extent:0.5 @ 1.0.
sub2 := View in:top.
sub2 viewBackground:Color yellow.
sub2 origin:0.5 @ 0 extent:0.5 @ 1.0.
top open
in this case, a one pixel wide fragment of the topViews black viewBackground will
be visible at the right border.
With relative corners, this problem is avoided; however, the second view may be one pixel wider:
|top sub1 sub2|
top := StandardSystemView extent:100@100.
top viewBackground:Color black.
sub1 := View in:top.
sub1 viewBackground:Color red.
sub1 origin:0 @ 0 corner:0.5 @ 1.0.
sub2 := View in:top.
sub2 viewBackground:Color yellow.
sub2 origin:0.5 @ 0 corner:1.0 @ 1.0.
top open
Most views compute a reasonable default extent when first created;
for buttons, this default is based upon the label given.
If you do not set the extent, this (view specific) default is used:
(you should read on, at least up to the chapter on panels, if you
do not like the placement of the buttons in the above example)
|v sub1 sub2|
v := StandardSystemView new.
v extent:300 @ 300.
sub1 := Button label:'button1' in:v.
sub1 origin:10 @ 10.
sub2 := Button label:'button2' in:v.
sub2 origin:10 @ 120.
v open
An example with relative extents is:
It is possible and often useful to combine a relative dimension with a pixel value.
|v sub1 sub2|
v := StandardSystemView extent:300 @ 300.
sub1 := Button label:'button1' in:v.
sub1 origin:0.1 @ 0.1.
sub1 extent:0.8 @ 0.4.
sub2 := Button label:'button2' in:v.
sub2 origin:0.1 @ 0.5.
sub2 extent:0.4 @ 0.4.
v open
here is an example which defines a relative width, but absolute pixel heights:
If you resize these topviews, the subviews position and/or dimensions
will be adjusted automatically.
|v sub1 sub2|
v := StandardSystemView extent:300 @ 300.
sub1 := Button label:'button1' in:v.
sub1 origin:0.1 @ 0.1.
sub1 extent:0.8 @ 100.
sub2 := Button label:'button2' in:v.
sub2 origin:0.1 @ 0.5.
sub2 extent:0.4 @ 100.
v open
If you specify a relative extent for a topview,
its extent is computed relative to the screen size:
creates a topview with half-width and half-height of the sceen.
(Notice, that some window managers insist on letting the user specify
the origin of the view - thus the origin argument may be ignored on
some systems).
|v|
v := StandardSystemView new.
v origin:(0.25 @ 0.25).
v corner:(0.75 @ 0.75).
v open
origin:
and corner:
-stuff):
|top b goodHeight|
top := StandardSystemView new.
top extent:(200 @ 200).
b := Button label:'hello' in:top.
goodHeight := b preferredExtent y.
b origin:(10 @ 10).
b corner:[ (top width - 10) @ (10 + goodHeight) ].
top open.
notice that we first ask the button about what it thinks is a good height
(based upon the fonts height) and remember this height.
Thus the button will have a fixed height, combined with a variable width.
If you base your computation on some other subviews position or size,
you should keep in mind that those blocks are evaluated in the order
in which the subviews were created within the superview.
Therefore, subview creation order may have an influence on the layout.
Example:
(uses the first buttons corner when computing the origin of the second button)
the following example demonstrates the effect of the evaluation order
and will not work correctly:
|top b1 b2 h1 h2|
top := StandardSystemView new.
top extent:(200 @ 200).
b1 := Button in:top.
b1 label:'hello'.
h1 := b1 preferredExtent y.
b1 origin:(10 @ 10).
b1 corner:[ (top width // 2 - 5) @ (10 + h1) ].
b2 := Button in:top.
b2 label:'wow'.
h2 := b2 preferredExtent y.
b2 origin:[ (b1 corner x + 5) @ 10 ].
b2 corner:[ (top width - 10) @ (10 + h2) ].
top open.
(because b2
's rule-block will always be evaluated
before b1
's rule,
while b2
depends on the value computed in b1
's rule.
Thus the old corner of b1
will be used in the computation of
b2
's origin.)
Hint:
|top b1 b2 h1 h2|
top := StandardSystemView new.
top extent:200 @ 200.
b2 := Button label:'wow' in:top.
h2 := b2 height.
b2 origin:[(b1 corner x + 5) @ 10].
b2 corner:[(top width - 10) @ (10 + h2)].
b1 := Button label:'hello' in:top.
h1 := b1 height.
b1 origin:(10 @ 10).
b1 corner:[(top width // 2 - 5) @ (10 + h1)].
top open.
You can force evaluation of these blocks (i.e. simulating a size-change) by sendingHint:#sizeChanged:nil
to the superview or#superViewChangedSize
to the view itself.
Although you can also specify the extent as relative extent it is not wise to do so, since rounding errors may lead to off-by-one pixel errors (for example, specifying a width of 0.5 for two side-by-side views, will produce a one-pixel error if the superviews width has an odd number of pixels. This possibly leads to an ugly looking layout, where views overlap or are not aligned correctly).Hint:
In contrast, relative corners do not show this problem and will produce a good looking result; of course, one of the views will be smaller by one pixel in this case.
In many cases, a block is not really required. If combined with insets (as described below), most layouts can be specified using relative sizes.
Also, have a look at the layout views (HorizontalPanelView
andVerticalPanelView
; these should solve most typical arrangements without a need to setup complicated dimension blocks.
As a basic framework, some layout classes are already provided with the system, but you can of course add your own ones for special requirements. The classes already available are:
origin:
settings can also be represented
by layoutOrigins)
It may be confusing, having another mechanism in addition to the previously described origin/corner methods. There are two reasons for having layout objects:
Lets see some examples; to place three buttons aligned along a vertical row,
we could write:
however, if these have different width, you may want to align
them along their horizontal centers (instead of their left borders):
|top b1 b2 b3|
top := StandardSystemView new.
top label:'3 buttons'.
top extent:200 @ 200.
b1 := Button label:'button1'.
b1 layout:(LayoutOrigin new
leftFraction:0.25;
topFraction:0.0).
top add:b1.
b2 := Button label:'butt2'.
b2 layout:(LayoutOrigin new
leftFraction:0.25;
topFraction:(1/3)).
top add:b2.
b3 := Button label:'this is button3'.
b3 layout:(LayoutOrigin new
leftFraction:0.25;
topFraction:(2/3)).
top add:b3.
top open
the above left the size as preferred by the buttons;
a layoutFrame controls both origin and extent:
|top b1 b2 b3|
top := StandardSystemView new.
top label:'3 buttons'.
top extent:200 @ 200.
b1 := Button label:'button1'.
b1 layout:(AlignmentOrigin new
leftFraction:0.5;
topFraction:0.0;
leftAlignmentFraction:0.5;
topAlignmentFraction:0.0).
top add:b1.
b2 := Button label:'butt2'.
b2 layout:(AlignmentOrigin new
leftFraction:0.5;
topFraction:(1/3);
leftAlignmentFraction:0.5;
topAlignmentFraction:0.0).
top add:b2.
b3 := Button label:'this is button3'.
b3 layout:(AlignmentOrigin new
leftFraction:0.5;
topFraction:(2/3);
leftAlignmentFraction:0.5;
topAlignmentFraction:0.0).
top add:b3.
top open
finally, you can add offsets (in pixels);
these fixed numbers are added to the views border position
after the relative computation.
These allow shifting a border and combining relative with
absolute values:
|top b1 b2 b3|
top := StandardSystemView new.
top label:'3 buttons'.
top extent:200 @ 200.
b1 := Button label:'button1'.
b1 layout:(LayoutFrame new
leftFraction:0.25;
topFraction:0.0;
rightFraction:0.75;
bottomFraction:(1/3)).
top add:b1.
b2 := Button label:'butt2'.
b2 layout:(LayoutFrame new
leftFraction:0.25;
topFraction:(1/3);
rightFraction:0.75;
bottomFraction:(2/3)).
top add:b2.
b3 := Button label:'this is button3'.
b3 layout:(LayoutFrame new
leftFraction:0.25;
topFraction:(2/3);
rightFraction:0.75;
bottomFraction:1).
top add:b3.
top open
Choose whichever layout specification fits your applications
needs best;
however, if you plan for portability with VisualWorks,
(or are used to VW), you may prefer to use layout objects.
|top b1 b2 b3|
top := StandardSystemView new.
top label:'3 buttons'.
top extent:200 @ 200.
b1 := Button label:'button1'.
b1 layout:(LayoutFrame new
leftFraction:0.25;
topFraction:0.0;
topOffset:5;
rightFraction:0.75;
bottomFraction:(1/3);
bottomOffset:-5).
top add:b1.
b2 := Button label:'butt2'.
b2 layout:(LayoutFrame new
leftFraction:0.25;
topFraction:(1/3);
topOffset:5;
rightFraction:0.75;
bottomFraction:(2/3);
bottomOffset:-5).
top add:b2.
b3 := Button label:'this is button3'.
b3 layout:(LayoutFrame new
leftFraction:0.25;
topFraction:(2/3);
topOffset:5;
rightFraction:0.75;
bottomFraction:1;
bottomOffset:-5).
top add:b3.
top open
Note:
The offsets in a layout are much like the insets, which will be described
below. However, offsets always shift the affected border down/right.
In contrast, insets always shift towards the center.
Note:
Of course, you can define your own layout class, which computes
any dimension you like. This may be useful for special arrangements.
In situations, where the default values are not acceptable,
read the value from the styleSheet (after all, its nothing more than
a table of name<->value associations). For example, if you have
a button which you think should show itself in red color, do not
hardcode Color red
into your application.
Instead, use something like:
StyleSheet colorAt:'mySpecialButtonsColor' default:Color red
so others can add an entry to the styleSheet(s):
...
mySpecialButtonsColor Color green
...
This avoids dictating your personal style onto other users.
As always, there are some exceptions to the above rule. For example, the default border for views is 1-pixel of black in the normal (i.e. non 3D) style. If you want to use a simple view for grouping (as described below), you usually do not want a border to be visible. In this case, you can set the border explicit to zero.
For very simple layouts, use just another subview, as in:
(please read on, if you think the example does not work ...)
|top frame1 frame2|
top := StandardSystemView label:'two views'.
top extent:300@300.
frame1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:top.
frame2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:top.
top open
Notice, that the origin and corners are given as floating point
(i.e. rational) numbers in the range [0..1].
In this case, they are interpreted as fraction of the superviews extent.
Remember:
When doing your first experiments, you may run into trouble when erroneously using
- integer values are interpreted in pixel,
- rationals as relative to the superviews size
- blocks are evaluated at resize time and should return an integer or rational which is taken in pixel or relative respectively.
"1"
instead of "1.0"
as
a dimension in the extent or corner parameter.
|top frame1 frame2|
top := StandardSystemView label:'two views'.
top extent:300@300.
frame1 := View origin:(0.1 @ 0.1) corner:(0.9 @ 0.5) in:top.
frame2 := View origin:(0.1 @ 0.6) corner:(0.9 @ 0.9) in:top.
frame1 level:-1.
frame2 level:-1.
top open
or:
|top frame1 frame2|
top := StandardSystemView label:'two views'.
top extent:300@300.
frame1 := View origin:(0.1 @ 0.1) corner:(0.9 @ 0.9) in:top.
frame2 := View origin:(0.25 @ 0.25) corner:(0.75 @ 0.75) in:frame1.
frame1 level:2.
frame2 level:-2.
top open
On non 3D view styles (see configuration), the level is ignored.
Here you can try:
|top frame1 frame2|
top := StandardSystemView label:'two views'.
top extent:300@300.
frame1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:top.
frame2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:top.
frame1 viewBackground:(Color grey:50).
frame2 viewBackground:(Color red:75 green:75 blue:25).
top open
Which also shows us how to use colors, and how a views background color is defined.
By the way:
the values given to #grey:
and #red:green:blue:
are interpreted in percent,
thus:
Color red:100 green:100 blue:0
is yellow;
while:
Color grey:25
is some darkish grey.
Color red
", "Color blue
" etc.
For those used to ST-80,
a compatibility class called ColorValue
is provided
which expects fractional arguments; thus you can also write
ColorValue red:1.0 green:1.0 blue:0.0
and
ColorValue brightness:0.25
Here, the arguments are in [0..1].
On black&white displays, Smalltalk has a hard time to try to get colors onto the screen - of
course. But at least it does its best it can (it will put a grey pattern, corresponding to
the colors brightness into the view).
On greyscale displays, a grey color corresponding to the colors brightness
will be used.
Thus, you really do not have to care for which type of display your
program will eventually run on. However, when designing your application,
you should keep in mind that others may have displays with less capabilities
than yours and the colors may be replaced by greyscales.
Never depend only on colors for highlighting or marking.
For example, red-on-green may produce a good contrast on your color screen,
but may not be visible on your friends greyscale or black&white display.
|top frame1 frame2|
top := StandardSystemView label:'two views'.
top extent:300@300.
frame1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:top.
frame2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:top.
frame1 viewBackground:(Image fromFile:'goodies/bitmaps/gifImages/garfield.gif').
frame2 viewBackground:(Image fromFile:'clients/Demos/bitmaps/hello_world.icon').
top open
Notice, that "Image fromFile:
" is able to find out the file format
itself.
The first image in the
above example is a depth-8 GIF encoded palette image,
while the second is a monochrome bitmap in
sun-icon file format.
Of course, the bitmaps can also be specified directly in your code
(but, it is always better,
to load them from a file - thereby allowing for more flexibility):
or:
|top|
top := StandardSystemView label:'a pattern'.
top extent:200@100.
top viewBackground:(Form width:8
height:8
fromArray:#(2r11001100
2r00110011
2r11001100
2r00110011
2r11001100
2r00110011
2r11001100
2r00110011)).
top open
As you see, constant bitmaps are defined in chunks of 8 pixels, left to right.
|top|
top := StandardSystemView label:'smile'.
top extent:200@100.
top viewBackground:(Form width:12
height:11
fromArray:#(
2r00000000 2r0000
2r00000000 2r0000
2r11000110 2r0000
2r11000110 2r0000
2r00000000 2r0000
2r00011000 2r0000
2r00011000 2r0000
2r00011000 2r0000
2r01000010 2r0000
2r01100110 2r0000
2r00111100 2r0000)).
top open
You can specify a colormap to be used with monochrome mitmaps too:
Just to show what is possible, try the following
(buttons and insets will be explained below in detail):
|top bitmap|
top := StandardSystemView label:'smile red/yellow'.
top extent:200@100.
bitmap := (Form width:12
height:11
fromArray:#(
2r00000000 2r0000
2r00000000 2r0000
2r11000110 2r0000
2r11000110 2r0000
2r00000000 2r0000
2r00011000 2r0000
2r00011000 2r0000
2r00011000 2r0000
2r01000010 2r0000
2r01100110 2r0000
2r00111100 2r0000)).
bitmap colorMap:(Array with:Color red "to be used for 0-bits"
with:Color yellow). "used for 1-bits"
top viewBackground:bitmap.
top open
In the previous example, another message (#backgroundColor:) was
used to change the buttons background color. To understand this,
let us first understand what the viewBackground is:
whenever a view is exposed or resized, the newly visible areas
are automatically filled with the viewBackground color (or pattern).
Later, the redraw handling method will actually draw any foreground
(text, images or whatever) when the exposure event arrives.
|v b granite wood|
granite := (Image fromFile:'libwidg3/bitmaps/granite.tiff').
wood := (Image fromFile:'libwidg3/bitmaps/woodH.tiff').
v := StandardSystemView label:'rock solid & wooden'.
v extent:300@300.
v viewBackground:granite.
b := Button label:'quit' in:v.
b backgroundColor:wood.
b activeBackgroundColor:wood.
b enteredBackgroundColor:wood.
b action:[v destroy].
b origin:(0.5 @ 0.5).
b leftInset:(b width // 2) negated.
b topInset:(b height // 2) negated.
v open.
Some views make a difference between the viewBackground, and the background
with which the contents is drawn - for example, a button might want its
edges to be drawn in grey (and therefore defines a viewBackground of grey),
but its label-background to be drawn in another color. To allow this,
button (and some others) define an additional method, called #backgroundColor:,
which changes the color with which the contents is drawn.
Here is such a button:
The default #backgroundColor: (implemented in a common view superclass)
sets the viewBackground.
|v b|
v := StandardSystemView label:'viewBackground vs. backgroundColor'.
v extent:400@300.
b := Button label:'bg blue' in:v.
b origin:0@0 corner:50@50.
b viewBackground:Color blue.
b backgroundColor:Color blue.
b := Button label:'bg blue / vb grey' in:v.
b origin:60@0 corner:110@50.
b viewBackground:Color grey.
b backgroundColor:Color blue.
v open.
Sometimes, you want to change the viewBackground of a complete views hierarchy
(i.e. of a view with all of its subviews). A concrete example is ST/X's
aboutBox, which changes the topViews background to some darkish grey
(and - of course - want all components viewBackgrounds to be also changed).
To do this, use #allViewBackground:, which walks down a views hierachy,
changing all viewbackgrounds:
|v l b t|
v := StandardSystemView label:'viewBackground'.
v extent:400@300.
l := Label label:'label' in:v.
l origin:5@5 corner:100@50.
b := Button label:'button' in:v.
b origin:5@60 corner:100@110.
t := ScrollableView for:TextView in:v.
t origin:110@5 corner:(1.0@1.0).
t contents:'hello, this is some text
foo
bar
baz
'.
v allViewBackground:(Color grey:30).
v open.
Notice, that insets combined with a relative dimension provide exactly the same functionality as provided by layout objects - so you should better use them right away. However, in many situations, insets are easier to use (especially if you are a beginner) and will still be supported for backward compatibility in the future.
For example, to create 2 subviews which take half of the superviews width,
AND have some constant 4-millimeter margin in between, use:
or:
|top sub1 sub2 mm|
mm := Display verticalPixelPerMillimeter rounded.
top := StandardSystemView label:'two views'.
top extent:300@300.
sub1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:top.
sub1 level:-1.
sub1 bottomInset:(mm * 2).
sub2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:top.
sub2 level:-1.
sub2 topInset:(mm * 2).
top open
(have a careful look at the labels definition - the insets
have negative values ...)
HINT:
|top sub1 sub2 lbl mm|
mm := Display verticalPixelPerMillimeter rounded.
top := StandardSystemView label:'wow'.
top extent:300@300.
sub1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:top.
sub1 level:-1.
sub1 allInset:mm.
sub1 bottomInset:mm // 2.
sub2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:top.
sub2 level:-1.
sub2 allInset:mm.
sub2 topInset:mm // 2.
sub2 bottomInset:mm * 10.
lbl := Label label:'info:' in:top.
lbl adjust:#left.
lbl level:-1.
lbl origin:(0.0 @ 1.0) corner:(1.0 @ 1.0).
lbl allInset:mm.
lbl topInset:(mm * 9) negated.
top open
As shown in the above example, negative insets are useful to place some view
at the bottom or at the right of its superview,
and you want a constant distance from that edge.
For example, the instance/class toggles in the browser could be
created this way (the inset is choosen so that the toggles are always at
the bottom):
Without negative insets, a somewhat complicated block
would be needed to compute the origin and size of those toggles
(taking care of round-off errors, odd sizes and borders ...)
|frame toggleI toggleC hI hC|
frame := View new.
frame extent:(300 @ 100).
toggleI := Toggle label:'instance' in:frame.
toggleC := Toggle label:'class' in:frame.
"
get the default (preferred) heights before changing
the extent ...
"
hI := toggleI height.
hC := toggleC height.
"
now set the origin to the corner to the bottom
(actually shrinking their height to 0 temporarily)
"
toggleI origin:(0.0 @ 1.0) corner:(0.5 @ 1.0).
toggleC origin:(0.5 @ 1.0) corner:(1.0 @ 1.0).
"
finally, set their top-inset to have them
appear at their preferred height from the bottom line
"
toggleI topInset:(hI negated).
toggleC topInset:(hC negated).
frame open.
All of the above are impossible to setup using only relative dimensions.
Of course, an alternative is to use panels. These will be described below in detail.
|top l1 l2|
top := StandardSystemView new.
top extent:200@200.
l1 := Label new.
l1 label:'hello'.
l1 origin:0.0 @ 0.0 corner:1.0 @ 0.5.
top add:l1.
l2 := Label new.
l2 label:(Image fromFile:'libtool/bitmaps/SBrowser.xbm').
l2 origin:0.0 @ 0.5 corner:1.0 @ 1.0.
top add:l2.
top open
in the above, the attributes of the labels were set individually,
for didactic reasons; to save you some typing, there are
also combination messages:
|top l1 l2|
top := StandardSystemView extent:200@200.
l1 := Label label:'hello' in:top.
l1 origin:0.0 @ 0.0 corner:1.0 @ 0.5.
l2 := Label label:(Image fromFile:'libtool/bitmaps/SBrowser.xbm') in:top.
l2 origin:0.0 @ 0.5 corner:1.0 @ 1.0.
top open
The default label uses a level of 0 and no border in all 3D
viewStyles and a borderWidth of 1 in non 3D styles.
|top l1 l2|
top := StandardSystemView extent:200@200.
l1 := Label label:'hello' in:top.
l1 origin:0.0 @ 0.0 corner:1.0 @ 0.5.
l1 level:-1.
l2 := Label label:(Image fromFile:'libtool/bitmaps/SBrowser.xbm') in:top.
l2 origin:0.0 @ 0.5 corner:1.0 @ 1.0.
l2 level:-1.
top open
#left
, #right
,
#center
(which is the default) or #fit
.
|top l1 l2|
top := StandardSystemView extent:200@200.
l1 := Label label:'hello' in:top.
l1 origin:0.0 @ 0.0 corner:1.0 @ (1/3).
l1 adjust:#left.
l1 level:-1.
l2 := Label label:(Image fromFile:'libtool/bitmaps/SBrowser.xbm') in:top.
l2 origin:0.0 @(1/3) corner:1.0 @ (2/3).
l2 adjust:#right.
l2 level:-1.
l2 := Label label:(Image fromFile:'libtool/bitmaps/SBrowser.xbm') in:top.
l2 origin:(1/3) @ (2/3) corner:(2/3) @ 1.0.
l2 allInset:5.
l2 adjust:#fit.
l2 level:-1.
top open
Since labels are often used to display some changing information string
(such as a current pathname), it may happen that the string is too
large to fit into the label.
To support this, labels offer special
adjusts which switch between a center adjust and one of right
or left adjust, depending on the strings size.
(this makes sense, with path names, where the right part is usually the
more interesting).
See what happens when you resize the following view (horizontically):
here, the first two strings are centered as long as they fits the views bounds,
but displayed right/left adjusted if they do not.
|top l1 l2 l3|
top := StandardSystemView extent:100@150.
l1 := Label label:'a very, very long label; could be a pathname' in:top.
l1 origin:0.0 @ 0.0 corner:1.0 @ (1/3).
l1 adjust:#centerRight.
l2 := Label label:'a name or other string, where the left is more interesting' in:top.
l2 origin:0.0 @ (1/3) corner:1.0 @ (2/3).
l2 adjust:#centerLeft.
l3 := Label label:'some other long label, with the default layout' in:top.
l3 origin:0.0 @ (2/3) corner:1.0 @ 1.0.
top open
As with other views, you can change the labels 3D appearance, border and viewBackground.
Additionally, you can change the foreground and background color
of the label.
Example:
Hint:
|top l|
top := StandardSystemView extent:100@100.
l := Label label:(Image fromFile:'libtool/bitmaps/SmalltalkX.xbm') in:top.
l origin:0.2 @ 0.2 corner:0.8 @ 0.8.
l foregroundColor:(Color green);
backgroundColor:(Color grey:20).
l level:-1.
top open
To avoid confusing the user, you should not use positive levels for labels, since they then look like buttons.
PanelView
,
HorizontalPanelView
and VerticalPanelView
.
Panels differ in the arrangement preference:
VerticalPanelView
always arranges its elements
top-to-bottom.
HorizontalPanelView
always arranges left-to-right.
Finally, the general PanelView
arranges
from top-left to bottom-right.
Try:
(I hope you already know some Smalltalk, to understand this ... :-)
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 300.
panel := VerticalPanelView origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
By default, the panel centers its elements with some 1mm (millimeter) spacing between the elements (try resizing the view). If they do not fit, the spacing is reduced. If they still do not fit, some elements may not be visible. Try resizing the view to see how elements get (re)arranged.
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 400.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel verticalLayout:#top. "not centered, but at top"
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
You can specify
#top
,
#topSpace
,
#bottom
,
#bottomSpace
,
#center
,
#spread
,
#spreadSpace
,
#fit
or
#fitSpace
as layout strategy.
#top
arranges elements at the top;
#bottom
at the bottom;
#center
places elements centered;
#spread
spreads them equally;
#fit
resizes the elements to fill the panel completely.
#xxxSpace
layouts
behave basically like their corresponding nonSpace layouts,
but start with a spacing (i.e. #top
positions the first element
right at the top border, while #topSpace
leaves some spacing
between the top border and the first element).
There are some more (obscure) layouts
(#fixLeft
and #leftFit
);
see the panel classes documentation for more info on these.
Try:
Try the above with
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 300.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel verticalLayout:#fit.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
#fit
replaced by any other layout and
see the difference.
With the exception of the #fit
layouts, a panel will leave
its elements extents unchanged - i.e. the elements should either provide a
reasonable preferredExtent or be sized correctly by the program.
In contrast, the #fit
layouts ignore the elements extent,
and force its size to fit the panel.
The twin of the VerticalPanelView
is the
HorizontalPanelView
, which offers the
same layout strategies, but does things horizontally.
The layouts supported by
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:550 @ 100.
panel := HorizontalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalLayout:#spread. "not centered, but at evenly distributed"
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
HorizontalPanelView
are
#left
,
#leftSpace
,
#right
,
#rightSpace
,
#center
,
#spread
,
#fit
.
and
#leftFit
.
The following gives a nice example of how powerful those settings
can be used and combined:
(However, notice that font scaling is a slow operation on most displays;
therefore consider the above example a non realistic demo)
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 500.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalLayout:#fitSpace.
panel verticalLayout:#fitSpace.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
(Button label:thisLabel in:panel) adjust:#fit
].
top open.
Finally, the general PanelView
arranges multiple rows,
but is currently not able to have the layout specified as detailed as above.
It simply fills itself with the elements starting top-left to
bottom-right.:
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 100.
panel := PanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
More examples.
change the space between elements:
or:
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 100.
panel := PanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalSpace:0.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
Of course, you can put any kind of view into a panel:
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 100.
panel := PanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalSpace:0.
panel verticalSpace:0.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
Adding empty views allows grouping:
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:160 @ 200.
panel := PanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalSpace:5.
panel verticalSpace:10.
#('one' 'two' 'three' 'four' 'five')
do:[:thisLabel |
Button label:thisLabel in:panel
].
(Label label:'label1' in:panel) level:-1.
(Label label:'label2' in:panel) level:1.
Toggle label:'toggle1' in:panel.
View extent:50@10 in:panel. "just an empty view"
#('six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
combining a layout of
|top panel|
top := StandardSystemView label:'buttons'.
top extent:350 @ 100.
panel := HorizontalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalSpace:0.
#('one' 'two' 'three')
do:[:thisLabel |
Button label:thisLabel in:panel
].
View extent:30@10 in:panel. "just a separator"
#('four' 'five')
do:[:thisLabel |
Toggle label:thisLabel in:panel
].
View extent:30@10 in:panel. "just a separator"
#('six' 'seven' 'eight')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
#fit
with an empty spacing,
gives you dense packing:
in a vertical panel, you can still control horizontal sizes of the elements
(and vice versa). Try:
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 300.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel verticalLayout:#fit.
panel verticalSpace:0.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
Button label:thisLabel in:panel
].
top open.
Usually, you would want to do something with those buttons later,
so better keep them around somewhere in a variable- as in:
|top panel|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 300.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel verticalLayout:#fit.
panel verticalSpace:0.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
|button|
button := Button label:thisLabel in:panel.
button width:1.0.
].
top open.
Can you imagine, what this does ? (try to find out before starting it :-)
|top panel buttons|
top := StandardSystemView label:'many buttons'.
top extent:100 @ 300.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
buttons := OrderedCollection new.
#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten')
do:[:thisLabel |
buttons add:(Button label:thisLabel in:panel)
].
top open.
(buttons at:5) disable.
(buttons at:4) action:[(buttons at:5) enable].
(buttons at:5) action:[(buttons at:5) disable].
You will find more examples in the classes' example category.
#fit
layout
(i.e. tell the panel to resize elements for tight packing)
AND specify a relative extent for
an element (i.e. tell the element to resize itself).
|top panel b|
top := StandardSystemView label:'bad button '.
top extent:200 @ 200.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalLayout:#fit.
b := Button label:'hello' in:panel.
b extent:(0.5 @ 40).
top open.
In the above, the button will show up with a half size (as expected),
but will change its size to full when the topView is first resized.
another bad example:
here the trouble is less obvious, since we only assign a new width,
which is 1.0 and should therefore not conflict with the panels decisions.
However, the
|top panel b|
top := StandardSystemView label:'bad button '.
top extent:200 @ 200.
panel := VerticalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
panel horizontalLayout:#fit.
panel verticalLayout:#fit.
b := Button label:'hello' in:panel.
b width:1.0.
top open.
#width:
method (currently) also changes the height
back to the buttons preferred height.
Therefore, the button will show its default height when mapped the first time.
After resizing the topView, the panel correctly recomputes the height and
the button will be shown in full size (as expected).
This seems to be a little bug in the #width:
method
and may be fixed in future versions.
For now, do not set relative or computed extents in elements
if the panel has a resizing layout (such as #fit
).
VariableHorizontalPanel
and
VariableVerticalPanel
.
Most browsers use these to allow for a variable ratio between
their selection list and their codeview.
|top panel subview1 subview2|
top := StandardSystemView label:'hello'.
top extent:400@400.
panel := VariableVerticalPanel origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
subview1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:panel.
subview2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:panel.
subview1 viewBackground:Color red.
subview2 viewBackground:(Image fromFile:'goodies/bitmaps/gifImages/garfield.gif').
top open
you may want to add some 3D effects, as in:
|top panel subview1 subview2|
top := StandardSystemView label:'hello'.
top extent:400@400.
panel := VariableVerticalPanel origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
subview1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:panel.
subview2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:panel.
subview1 level:-1.
subview2 level:-1.
top open
or (just a try):
|top panel subview1 subview2|
top := StandardSystemView label:'hello'.
top extent:400@400.
panel := VariableVerticalPanel origin:(10 @ 10)
corner:[(top width - 10) @ (top height - 10)]
in:top.
subview1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:panel.
subview2 := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:panel.
panel level:3.
subview1 level:-1.
subview2 level:-1.
top open
Although not being very beautiful, the above example shows how a views corner can also be given by a
computation rule.
Whenever the topView is resized, the subview will recompute its corner,
by evaluating the corner-block. This also works for origin and extent.
Using blocks as rules provides a most powerful and flexible way to specify view
dimensions.
Variable panels require their subviews to have relative origins and
corners (or extends). If you want to add constant size subviews, you
have to use (currently) a helper view:
See:
to show a more complex example,
the following puts constant height button panels in between
the variable size views:
|top panel helper subview1 subview2 subview3|
top := StandardSystemView label:'hello'.
top extent:400@400.
panel := VariableVerticalPanel origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
subview1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.5) in:panel.
subview1 viewBackground:Color red.
helper := View origin:(0.0 @ 0.5) corner:(1.0 @ 1.0) in:panel.
subview2 := View origin:(0.0 @ 0.0) corner:(1.0 @ 20) in:helper.
subview2 viewBackground:Color green.
subview3 := View origin:(0.0 @ 20) corner:(1.0 @ 1.0) in:helper.
subview3 viewBackground:Color blue.
subview1 level:-1.
helper level:-1.
top open
|top panel helper1 subjectview helper2
buttonPanel1 buttonPanel2 letterview b|
top := StandardSystemView label:'Mail'.
panel := VariableVerticalPanel
origin:(0.0 @ 0.0) corner:(1.0 @ 1.0) in:top.
helper1 := View origin:(0.0 @ 0.0) corner:(1.0 @ 0.4) in:panel.
buttonPanel1 := HorizontalPanelView
origin:(0.0 @ 0.0) corner:(1.0 @ 40) in:helper1.
buttonPanel1 horizontalLayout:#leftSpace.
b := Button label:'delete' in:buttonPanel1.
b := Button label:'new mail' in:buttonPanel1.
(View in:buttonPanel1) extent:30@1; borderWidth:0; level:0. "for spacing"
b := Button label:'exit' in:buttonPanel1.
b action:[top destroy].
subjectview := ScrollableView for:SelectionInListView in:helper1.
subjectview origin:(0.0 @ 40) corner:(1.0 @ 1.0).
subjectview list:#('letter1' 'letter2' 'letter3' '...' 'last letter').
helper2 := View origin:(0.0 @ 0.4) corner:(1.0 @ 1.0) in:panel.
buttonPanel2 := HorizontalPanelView
origin:(0.0 @ 0.0) corner:(1.0 @ 40) in:helper2.
buttonPanel2 horizontalLayout:#leftSpace.
b := Button label:'reply' in:buttonPanel2.
b := Button label:'print' in:buttonPanel2.
letterview := ScrollableView for:TextView in:helper2.
letterview origin:(0.0 @ 40) corner:(1.0 @ 1.0).
top open
#beInvisible
and made visible again by #beVisible
.
|top topFrame check list|
top := StandardSystemView new.
top extent:150@400.
topFrame := VerticalPanelView origin:0.0@0.0 corner:1.0@0.4 in:top.
topFrame horizontalLayout:#leftSpace.
topFrame add:(check := CheckBox label:'hidden').
check pressAction:[list beInvisible].
check releaseAction:[list beVisible].
list := ScrollableView for:SelectionInListView.
list origin:0.0@0.4 corner:1.0@1.0.
list list:#('foo' 'bar' 'baz').
top add:list.
check turnOn.
list beInvisible.
top open
Invisible views do not react to any user events; if you need response to
button or keyboard events in a hidden views display area,
you have to place an instance of InputView there. These special views are
transparent, and cannot be drawn into. However, all user events are processed
as usual; these are typically delegated to some other object.
Example:
|top topFrame check list inputOnly eventReceiver|
top := StandardSystemView new.
top extent:150@400.
topFrame := VerticalPanelView origin:0.0@0.0 corner:1.0@0.4 in:top.
topFrame horizontalLayout:#leftSpace.
topFrame add:(check := CheckBox label:'covered').
check pressAction:[list lower].
check releaseAction:[list raise].
list := ScrollableView for:SelectionInListView.
list origin:0.0@0.4 corner:1.0@1.0.
list list:#('foo' 'bar' 'baz').
top add:list.
eventReceiver := Plug new.
eventReceiver respondTo:#handlesButtonPress:inView:
with:[:button :view | true].
eventReceiver respondTo:#buttonPress:x:y:view:
with:[:button :x :y :view | check turnOff. list raise].
inputOnly := InputView origin:0.0@0.4 corner:1.0@1.0 in:top.
inputOnly delegate:eventReceiver.
check turnOn.
top open
The above is an artificial example, InputViews main use is to cover complete
application views to catch all incoming events
(for example, in window builder like applications).
Example (this examples uses elements which will be explained later - simply
concentrate on the view creation and the raise-message sent
from the buttons):
Of course, the subviews dimensions and positions can be arbitrary
(however, except in noteStack-like applications, it does not make much
sense to NOT align the views).
|top viewStack buttonPanel sub1 sub2|
top := StandardSystemView label:'really two views'.
top extent:300 @ 350.
buttonPanel := HorizontalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 50)
in:top.
(Button label:'view1' action:[sub1 raise] in:buttonPanel).
(Button label:'view2' action:[sub2 raise] in:buttonPanel).
viewStack := View
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
viewStack topInset:(buttonPanel height).
sub1 := TextView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:viewStack.
sub1 contents:'Hello, I am the TextView (sub1)'.
sub2 := ClockView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:viewStack.
top open
as in:
If you want to implement noteStack-like applications, you can use the
event delegation mechanism, to catch events and raise when clicked-upon.
|top viewStack buttonPanel sub1 sub2 sub3|
top := StandardSystemView new.
top extent:300 @ 350.
buttonPanel := HorizontalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 50)
in:top.
(Button label:'view1' action:[sub1 raise] in:buttonPanel).
(Button label:'view2' action:[sub2 raise] in:buttonPanel).
(Button label:'view3' action:[sub3 raise] in:buttonPanel).
viewStack := View
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
viewStack topInset:(buttonPanel height).
sub1 := TextView
origin:(0.1 @ 0.1)
corner:(0.75 @ 0.75)
in:viewStack.
sub1 contents:'Hello, I am the TextView (sub1)'.
sub1 level:0; borderWidth:1.
sub2 := ClockView
origin:(0.25 @ 0.25)
corner:(0.9 @ 0.9)
in:viewStack.
sub2 level:0; borderWidth:1.
sub3 := View
origin:(0.2 @ 0.2)
corner:(0.8 @ 0.8)
in:viewStack.
sub3 viewBackground:Color red.
sub3 level:0; borderWidth:1.
top open
Since different view classes have different default-borders and 3D
levels, you may have to set these explicit in this kind of application.
The following example adds some
more fancy stuff to the above demo. (notice, that each subview comes with
its correct middleButtonMenu - and that you can modify the drawViews
elements as in the DrawTool).
Try:
|top viewStack buttonPanel l sub1 sub2 sub3 sub4|
top := StandardSystemView new.
top extent:300 @ 350.
buttonPanel := HorizontalPanelView
origin:(0.0 @ 0.0)
corner:(1.0 @ 50)
in:top.
l := Label label:'4' in:buttonPanel.
(View in:buttonPanel) width:10.
(Button label:'view1' action:[l label:'1'. sub1 raise] in:buttonPanel).
(Button label:'view2' action:[l label:'2'. sub2 raise] in:buttonPanel).
(Button label:'view3' action:[l label:'3'. sub3 raise] in:buttonPanel).
(Button label:'view4' action:[l label:'4'. sub4 raise] in:buttonPanel).
viewStack := View
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
viewStack topInset:(buttonPanel height).
viewStack level:0.
sub1 := TextView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:viewStack.
sub1 contents:'I am the TextView (sub1)'.
sub1 level:-1; borderWidth:0.
sub2 := ScrollableView for:EditTextView in:viewStack.
sub2 origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0).
sub2 contents:'I am the EditTextView (sub2)'.
sub2 level:0; borderWidth:0.
sub3 := DrawView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:viewStack.
sub3 level:-1; borderWidth:0.
sub3 add:(DrawEllipse new
origin:(50 @ 50) corner:(250 @ 250);
foreground:(Color green);
background:(Color black);
fillPattern:(Image fromFile:'libtool/bitmaps/SmalltalkX.xbm')
).
sub3 add:(DrawRectangle new
origin:(50 @ 50) corner:(250 @ 250);
foreground:(Color green);
fillPattern:nil
).
sub4 := ClockView
origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:viewStack.
sub4 level:-1; borderWidth:0.
top open
widthOfContents
heightOfContents
Currently, due to historical reasons, the ListView
class and its subclasses use a different
mechanism to implement scrolling (they keep track of the scroll origin themselves - not using
transformations). This implementation is a leftover from times when no transformation existed
and TextViews were the only views which supported scrolling.
The implementation of these will be changed in the next release, to have a consistent
implementation over all views.
Button
is a superclass of Toggle
,
CheckToggle
and RadioButton
, the following is
also valid for these subclasses which are described in more detail below.
(Note to experienced ST-80 users: in many applications, a setup using actionBlocks can be done with less coding and is probably easier to understand for beginners. You may of course continue to use buttons in the MVC way you are used to. Or, as we suggest, to use actionBlocks for simple setups and change messages in situations, where mutliple buttons operate on a common or complex model).
Action example:
By default, buttons fire (i.e. perform their action) when the
mouse button is released. This behavior is usually better for the user,
since he/she can change her mind and leave the buttons screen area before
releasing the mouse button
(i.e. the ``oops - I don't want this to be done'' situation).
|top b|
top := StandardSystemView label:'a button'.
top extent:100@100.
b := Button in:top.
b label:'press me'.
b origin:0.1@0.1 corner:0.9@0.9.
b action:[Transcript showCR:'hello there'].
top open
In some situations you may want your button to fire immediately
(for example, the scrollbars arrow buttons do so).
Since user interaction is really done by another object (the buttons controller),
you have to tell this one when to fire:
Actually, buttons really support two actions: the pressAction and
the releaseAction.The
|top b|
top := StandardSystemView label:'a button'.
top extent:100@100.
b := Button in:top.
b label:'press me'.
b origin:0.1@0.1 corner:0.9@0.9.
b controller beTriggerOnDown.
b action:[Transcript showCR:'hello there'].
top open
#beTriggerOnDown
message simply
arranges that further action settings are installed as pressAction,
while the default arranges them to be installed as releaseAction.
This implies,
that you have to set the action after
sending the #beTriggerOnDown
message.
You can also specify both or explicit press- and releaseActions;
the one set with action:
is the pressAction if its a triggerOnUp
button, or the releaseAction if its a triggerOnDown button.
try:
or:
(Button label:'see the transcript') pressAction:[Transcript showCR:'pressed'; endEntry];
releaseAction:[Transcript showCR:'released'; endEntry];
open.
Transcript topView raise
|p|
p := HorizontalPanelView new.
(Button label:'up/down' in:p)
pressAction:[Transcript topView raise];
releaseAction:[Transcript topView lower].
(Button label:'up on press' in:p)
pressAction:[Transcript topView raise].
(Button label:'up on release' in:p)
releaseAction:[Transcript topView raise].
p extent:p preferredExtent.
p open
Hint (read before you start to change fonts in your buttons ;-):Buttons (like all other views) have a font in which they draw text. You can set the font to be used with the
do not play around too much (if at all) with different font styles and/or font sizes - it usually makes a user interface worse and harder to use for others. Do not use decorative fonts (such as gothic or old-english). Finally, do not depend on the font being available on all machines.
You can almost always assume that 'times', 'courier' and 'helvetica' are available; other fonts may not be present in all windowing systems.
#font:
message.
The default font to use for all buttons is defined in the styleSheet.
|top panel b1 b2|
top := StandardSystemView label:'two buttons'.
top extent:200 @ 100.
panel := HorizontalPanelView origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
b1 := Button label:'one' in:panel.
b1 font:(Font family:'helvetica' face:'bold' style:'roman' size:24).
b2 := Button label:'two' in:panel.
b2 font:(Font family:'helvetica' face:'bold' style:'roman' size:8).
top open
The Smalltalk/X's Font class is smart enough to detect
non existing fonts (and provide some default fall-back then):
|top b|
top := StandardSystemView label:'a button'.
top extent:200@200.
b := Button label:'one' in:top.
b font:(Font family:'funnyFont' face:'bold' style:'roman' size:24).
top open
Buttons can have image-labels instead of textual labels:
|top panel b1 b2 b3 b4|
top := StandardSystemView label:'many buttons'.
top extent:200 @ 100.
panel := HorizontalPanelView origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
b1 := Button label:'one' in:panel.
b2 := Button label:'two' in:panel.
b3 := Button label:(Image fromFile:'libtool/bitmaps/Camera.xbm') in:panel.
b4 := Button label:'bye bye' in:panel.
b4 action:[top destroy].
top open
|top panel b1 b2 b3 b4|
top := StandardSystemView label:'many buttons'.
top extent:250 @ 100.
panel := HorizontalPanelView origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
b1 := Button label:'one' in:panel.
b2 := Button label:(Image fromFile:'goodies/bitmaps/xpmBitmaps/device_images/ljet3.xpm') in:panel.
b3 := Button label:(Image fromFile:'libtool/bitmaps/Camera.xbm') in:panel.
b4 := Button label:'bye bye' in:panel.
b1 action:[b3 enable. b4 enable].
b2 action:[b3 disable].
b3 action:[b4 disable].
b4 action:[top destroy].
top open.
Notice that except for toggles and radioButtons,
explicit turning on/off of buttons is rarely required.
Beginners may ignore the following:
Enabling can also be done via a so called enableChannel.
This is a valueHolder object, which automatically informs other
objects about any changes of its value. Especially in complex
applications, use of an enableChannel may simplify things, since you don't
have to enable/disable all buttons manually.
Example:
|top panel ena t b1 b2 b3 check|
top := StandardSystemView new.
top extent:300@100.
panel := HorizontalPanelView origin:0.0 @ 0.0 corner:1.0 @ 1.0 in:top.
ena := false asValue.
t := Toggle label:'enable' in:panel.
t model:ena.
b1 := Button label:'button1' in:panel.
b1 controller enableChannel:ena.
b2 := Button label:'button2' in:panel.
b2 controller enableChannel:ena.
b3 := Button label:'button3' in:panel.
b3 controller enableChannel:ena.
top open.
check := CheckBox model:ena.
check label:'also enable'.
check extent:(check preferredExtent + (5@5)).
check open
You can specify the foreground/background colors for the passive state,
the active state (i.e. when pressed) and the entered state (i.e. when the
mouse-pointer is in the button).
Usually, you should let buttons use their default values (which come
from the styleSheet). But, for special applications, it may be useful
to change those.
Try:
Beside the buttons interface,
these examples gave us some more new information:
|top panel b1 b2 b3 b4|
top := StandardSystemView label:'many buttons'.
top extent:200 @ 100.
panel := HorizontalPanelView origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
b1 := Button label:'one' in:panel.
b2 := Button label:'two' in:panel.
b3 := Button label:(Image fromFile:'libtool/bitmaps/Camera.xbm') in:panel.
b4 := Button label:'bye bye' in:panel.
b1 action:[b3 turnOn.
b4 enable.
b4 backgroundColor:(Color red lightened).
b4 enteredBackgroundColor:(Color red).
].
b2 action:[b3 turnOff].
b3 action:[b4 disable];
foregroundColor:Color blue;
backgroundColor:Color red.
b4 action:[top destroy].
top open.
Transcript
is actually the one
subview showing the text - not the StandardSystemView around it -
try Transcript inspect
and
follow the superView instance variables till you get to this topview.
Sending topView to a real topview does not hurt, it will return itself.)
|v b1 b2 winBitmapsPath|
v := HorizontalPanelView new.
v extent:200 @ 100.
winBitmapsPath := 'goodies/bitmaps/winBitmaps'.
b1 := Button in:v.
b1 borderWidth:0; level:0; onLevel:0; offLevel:0.
b1 activeLogo:((Image fromFile:winBitmapsPath , '/setup_down.bmp') onDevice:Display).
b1 passiveLogo:((Image fromFile:winBitmapsPath , '/setup_up.bmp') onDevice:Display).
b2 := Button in:v.
b2 borderWidth:0; level:0; onLevel:0; offLevel:0.
b2 activeLogo:((Image fromFile:winBitmapsPath , '/help_down.bmp') onDevice:Display).
b2 passiveLogo:((Image fromFile:winBitmapsPath , '/help_up.bmp') onDevice:Display).
v open
Note:
Note:
By default, labels (and therefore buttons too), will inset the
logo by some pixels and draw themself in 3D depending on the style.
If the bitmaps already have the 3D style included (as in the above example),
you should setup the button accordingly.
This is done by:
someButton
borderWidth:0;
onLevel:0;
offLevel:0;
orizontalSpace:0;
verticalSpace:0.
or, for short:
someButton beImageButton
So, the above example looks better if we write:
|v b1 b2 winBitmapsPath|
v := HorizontalPanelView new.
v extent:200 @ 100.
winBitmapsPath := 'goodies/bitmaps/winBitmaps'.
b1 := Button in:v.
b1 activeLogo:((Image fromFile:winBitmapsPath , '/setup_down.bmp') onDevice:Display).
b1 passiveLogo:((Image fromFile:winBitmapsPath , '/setup_up.bmp') onDevice:Display).
b1 beImageButton.
b2 := Button in:v.
b2 activeLogo:((Image fromFile:winBitmapsPath , '/help_down.bmp') onDevice:Display).
b2 passiveLogo:((Image fromFile:winBitmapsPath , '/help_up.bmp') onDevice:Display).
b2 beImageButton.
v open
You can even define separate logos to us if the button is disabled
or has the focus (i.e. has been tabbed active):
|v b1 b2 winBitmapsPath|
v := HorizontalPanelView new.
v extent:200 @ 100.
b1 := Button in:v.
b1 borderWidth:0; level:0; onLevel:0; offLevel:0.
b1 horizontalSpace:0; verticalSpace:0.
winBitmapsPath := 'goodies/bitmaps/winBitmaps'.
b1 activeLogo:((Image fromFile:winBitmapsPath , '/prev_down.bmp') onDevice:Display).
b1 passiveLogo:((Image fromFile:winBitmapsPath , '/prev_up.bmp') onDevice:Display).
b1 disabledLogo:((Image fromFile:winBitmapsPath , '/prev_disabled.bmp') onDevice:Display).
b2 := Button in:v.
b2 borderWidth:0; level:0; onLevel:0; offLevel:0.
b2 horizontalSpace:0; verticalSpace:0.
b2 activeLogo:((Image fromFile:winBitmapsPath , '/next_down.bmp') onDevice:Display).
b2 passiveLogo:((Image fromFile:winBitmapsPath , '/next_up.bmp') onDevice:Display).
b2 disabledLogo:((Image fromFile:winBitmapsPath , '/next_disabled.bmp') onDevice:Display).
v open.
(Delay forSeconds:5) wait.
b1 disable.
(Delay forSeconds:5) wait.
b1 enable.
b2 disable
Another example; here, we extract the background color for the view from the button image:
|v b1 winBitmapsPath img|
v := HorizontalPanelView new.
v extent:200 @ 100.
b1 := Button in:v.
b1 borderWidth:0; level:0; onLevel:0; offLevel:0.
b1 horizontalSpace:0; verticalSpace:0.
winBitmapsPath := 'goodies/bitmaps/winBitmaps'.
b1 activeLogo:(img := ((Image fromFile:winBitmapsPath , '/replay_down.bmp') onDevice:Display)).
b1 passiveLogo:((Image fromFile:winBitmapsPath , '/replay_up.bmp') onDevice:Display).
b1 focusLogo:((Image fromFile:winBitmapsPath , '/replay_focus.bmp') onDevice:Display).
b1 controller beTriggerOnDown.
v viewBackground:(img at:0@0).
b1 action:[
b1 withCursor:Cursor wait
do:[ SoundStream playSoundFile:'/usr/local/lib/sounds/laugh.snd']
].
v open.
The above also works with strings:
|v b1 t ena|
v := HorizontalPanelView new.
v extent:200 @ 100.
t := Toggle label:'enable' in:v.
ena := true asValue.
t model:ena.
b1 := Button in:v.
b1 activeLogo:'release me'.
b1 passiveLogo:'press me'.
b1 disabledLogo:'sorry '.
b1 enableChannel:ena.
v open
However, this shows a little problem: the button resizes itself, to make
the bigger logo fully visible.
sizeFixed:true
to it.
This method will freeze the current buttons size. You should do so
after you defined the largest logo that will ever
appear in it.
(actually, since all of this is defined in buttons superclass:
Label
, all of this is also true for labels).
|v b|
v := HorizontalPanelView new.
v extent:200 @ 100.
b := Button in:v.
"
set to the largest logo - just for the fixing
"
b logo:'release me'.
b sizeFixed:true.
b activeLogo:'release me'.
b passiveLogo:'press me'.
v open
BTW:
#center
.
But the above example may also look good with left adjusted logos.
#left
, #right
,
#center
, #centerLeft
or #centerRight
.
#centerLeft
or #centerRight
also center their
logo, but change the adjustment to left or right resp. if the logo does
not fit. (i.e. use these if your logo may become very long, to tell the
label/button which part should be shown in this case).
|v b|
v := HorizontalPanelView new.
v extent:200 @ 100.
b := Button in:v.
"
set to the largest logo - just for the fixing
"
b logo:'release me'.
b sizeFixed:true.
b adjust:#left.
b activeLogo:'release me'.
b passiveLogo:'press me'.
v open
|top panel b1 b2 b3 b4|
top := StandardSystemView label:'many buttons'.
top extent:200 @ 100.
panel := HorizontalPanelView origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
b1 := Button label:'one' in:panel.
b2 := Toggle label:'two' in:panel.
b3 := Button label:(Image fromFile:'libtool/bitmaps/Camera.xbm') in:panel.
b4 := Button label:'bye bye' in:panel.
b1 action:[b3 turnOn.].
b2 pressAction:[b4 enable].
b2 releaseAction:[b4 disable].
b4 action:[top destroy].
b4 disable.
top open.
Sometimes, you want to arrange toggles in a group, such that only one of them may be
on at any time. This is done by using radio buttons, and an instance of
its companion class, a RadioButtonGroup:
|top panel b1 b2 b3 b4 b5 group|
top := StandardSystemView label:'one only buttons'.
top extent:200 @ 100.
panel := HorizontalPanelView origin:(0.0 @ 0.0)
corner:(1.0 @ 1.0)
in:top.
b1 := RadioButton label:'one' in:panel.
b2 := RadioButton label:'two' in:panel.
b3 := RadioButton label:'three' in:panel.
b4 := RadioButton label:(Image fromFile:'libtool/bitmaps/Camera.xbm') in:panel.
b5 := Button label:'exit' in:panel.
group := RadioButtonGroup new.
group add:b1;
add:b2;
add:b3;
add:b4.
b5 action:[top destroy].
top open.
If you want one of those buttons to be ON initially, add a line such as:
b3 turnOn.
to the above setup.
|top panel check1 check2 check3 grp|
top := StandardSystemView new.
top extent:200@200.
panel := VerticalPanelView origin:0.0@0.0 corner:1.0@1.0 in:top.
panel verticalLayout:#spreadSpace.
check1 := CheckToggle in:panel.
check2 := CheckToggle in:panel.
check3 := CheckToggle in:panel.
grp := RadioButtonGroup new.
grp add:check1.
grp add:check2.
grp add:check3.
top open
CheckToggles do not display a label - this is an intended limitation,
since there are uses for these toggles without a label:
|top panel hpanel check1 check2 check3 grp
readPermission writePermission executePermission|
readPermission := false asValue.
writePermission := false asValue.
executePermission := false asValue.
top := StandardSystemView new.
top extent:200@200.
panel := VerticalPanelView origin:0.0@0.0 corner:1.0@1.0 in:top.
panel verticalLayout:#topSpace.
hpanel := HorizontalPanelView new.
hpanel add:(Label label:'read').
hpanel add:(Label label:'write').
hpanel add:(Label label:'execute').
hpanel extent:1.0@(hpanel preferredExtent y); horizontalLayout:#fitSpace.
panel add:hpanel.
hpanel := HorizontalPanelView new.
hpanel add:(CheckToggle on:readPermission).
hpanel add:(CheckToggle on:writePermission).
hpanel add:(CheckToggle on:executePermission).
hpanel extent:1.0@(hpanel preferredExtent y); horizontalLayout:#spreadSpace.
panel add:hpanel.
top open
For the common case, where a label is to be displayed aside the checkmark,
use CheckBox instead; this wraps a checkToggle with a label and displays them
side by side. (Technically, this is a subclass of HorizontalPanel, so it responds
to all layout messages as described above).
|top panel check1 check2 check3
readPermission writePermission executePermission|
readPermission := true asValue.
writePermission := false asValue.
executePermission := false asValue.
top := StandardSystemView new.
top extent:200@200.
panel := VerticalPanelView origin:0.0@0.0 corner:1.0@1.0 in:top.
panel verticalLayout:#spreadSpace.
panel horizontalLayout:#leftSpace.
check1 := CheckBox label:'read' in:panel.
check2 := CheckBox label:'write' in:panel.
check3 := CheckBox label:'execute' in:panel.
check1 model:readPermission.
check2 model:writePermission.
check3 model:executePermission.
top open
of course, these can also be used with radioButton behavior:
|top panel check1 check2 check3 grp|
top := StandardSystemView new.
top extent:200@200.
panel := VerticalPanelView origin:0.0@0.0 corner:1.0@1.0 in:top.
panel verticalLayout:#spreadSpace.
panel horizontalLayout:#leftSpace.
check1 := CheckBox label:'pizza' in:panel.
check2 := CheckBox label:'spaghetti' in:panel.
check3 := CheckBox label:'lasagne' in:panel.
grp := RadioButtonGroup new.
grp add:check1 value:#pizza.
grp add:check2 value:#spaghetti.
grp add:check3 value:#lasagne.
top openModal.
Transcript showCR:grp value
As a side effect, the above example demonstrates how a radioButtonGroup
can operate on some model.
This may be more convenient than using actionBlocks in some applications.
|top panel check1 check2 check3 grp selection|
top := StandardSystemView new.
top extent:200@200.
panel := VerticalPanelView origin:0.0@0.0 corner:1.0@1.0 in:top.
panel verticalLayout:#spreadSpace.
panel horizontalLayout:#leftSpace.
check1 := CheckBox label:'pizza' in:panel.
check1 action:[selection := #pizza].
check2 := CheckBox label:'spaghetti' in:panel.
check2 action:[selection := #spaghetti].
check3 := CheckBox label:'lasagne' in:panel.
check3 action:[selection := #lasagne].
grp := RadioButtonGroup new.
grp add:check1.
grp add:check2.
grp add:check3.
top openModal.
Transcript showCR:selection
Use whichever seems more natural to you, or fits easier into your concrete
application.
The thumbs position (in percent, 0..100) is passed as argument to the actionBlock
- therefore we need a block which expects one argument as the scrollAction:
Note:
|sl|
sl := Slider new.
sl extent:20 @ 200. "notice: some window managers ignore this"
sl scrollAction:[:percent | Transcript showCR:('moved to ' , percent rounded printString) ].
sl open.
I should have called it "slideAction";
but Slider is inheriting most of its protocol
from Scroller; and thats where the whole show is actually performed.
Another Note:
We won't go too deeply into Scrollers here - they need more information about the
size and position of the thing that is scrolled; if you want to try a standalone scroller,
try
and experiment by sending the scroller thumbHeight:
and thumbOrigin:
messages.
Pass numerical percentage-values as arguments.
A funny example:
|s|
s := Scroller extent:20 @ 200.
s scrollAction:[:percent | Transcript showCR:('moved to ' , percent rounded printString) ].
s thumbHeight:50.
s thumbOrigin:10.
s open.
You will notice, that a scroller decides to show nothing for a size of 100% -
that is also the behavior of Scrollbars in all of your textviews.
|sl sc|
sl := Slider extent:20 @ 200.
sc := HorizontalScroller extent:200 @ 20.
sl scrollAction:[:percent | sc thumbHeight:percent].
sc scrollAction:[:percent | sl thumbOrigin:percent].
sl open.
sc open.
By default, a sliders (and scrollers) value range is 0 .. 100 (percent).
In some applications, it may be more convenient to change this and
let the slider report values which are already scaled to
your applications requirements.
The following sets this range to -50 .. +50:
Cousins of Sliders are the so called SteppingSliders;
these add two buttons, to increase/decrease the sliders value.
(they are like scrollBars, but use a slider instead of scroller for
their thumb component).
|sl|
sl := Slider new.
sl extent:20 @ 200.
sl scrollAction:[:val | Transcript showCR:('moved to ' , val rounded printString) ].
sl start:-50 stop:50.
sl open.
For example:
if you prefer '+'/'-' as button labels (instead of the arrows),
change the buttons labels with:
|top sl|
top := StandardSystemView new.
top extent:200@200.
sl := HorizontalSteppingSlider in:top.
sl width:1.0.
sl start:-50 stop:50 step:2.
sl thumbOrigin:0.
sl scrollAction:[:val | Transcript showCR:('moved to ' , val rounded printString) ].
top open.
(Notice: the scrollBar is normally not prepared for the buttons to change their
size - therefore, we force a recomputation of the elements positions in the
above example).
|top sl|
top := StandardSystemView new.
top extent:200@200.
sl := HorizontalSteppingSlider in:top.
sl upButton label:'+'.
sl downButton label:'-'.
sl setElementPositions.
sl width:1.0.
sl start:-50 stop:50 step:2.
sl thumbOrigin:0.
sl scrollAction:[:val | Transcript showCR:('moved to ' , val rounded printString) ].
top open.
ScrolledView
, which do all setup for you).
Anyway, it may be interesting to use a standalone scrollbar sometimes:
|v s|
v := View extent:100 @ 200.
s := ScrollBar in:v.
s height:1.0. "only changing height - let width stay its default"
s scrollAction:[:percent | Transcript showCR:('moved to ' , percent rounded printString) ].
s scrollUpAction:[Transcript showCR:'one step up' ].
s scrollDownAction:[Transcript showCR:'one step down' ].
s thumbHeight:50.
s thumbOrigin:10.
v open.
In addition to the scrollers scrollAction,
a scrollbar defines two additional actions:
scrollUpAction which gets evaluated when the scrollup button is pressed;
and scrollDownAction which gets evaluated when the scrolldown button is pressed;
Of course, the scrollbar has no idea of "how much" one step is in this isolated example,
so the scroller is not updated when the step-up and step-down buttons are pressed.
We have to tell it (in this example):
But again, this is not the normal use of scrollbars - usually they are connected to some view
which calls:
|v s|
v := View extent:100 @ 200.
s := ScrollBar in:v.
s height:1.0.
s scrollAction:[:percent | Transcript showCR:('moved to ' , percent rounded printString) ].
s scrollUpAction:[
Transcript showCR:'one step up'.
s thumbOrigin:(s thumbOrigin - 10)
].
s scrollDownAction:[
Transcript showCR:'one step down'.
s thumbOrigin:(s thumbOrigin + 10)
].
s thumbHeight:50.
s thumbOrigin:10.
v open.
s setThumbFor:self
whenever some change takes place. Typically, this is an instance of ScrollableView
,
doing so whenever its scrolled-view (which does not really know about being scrolled) sends a
self contentsChanged
or self originChanged
.
|v t|
v := ScrollableView new.
t := TextView new.
v scrolledView:t.
t contents:('/etc/hosts' asFilename readStream contents).
v open
The scrollableView as created above, is the wrapper view, which
is able to scroll a single view. Any view can be placed into a
scrollableView, as long as it provides the heightOfContents
,
widthOfContents
and scrollTo:
protocol
(which is inherited from View
- thus understood by any
view).
Since the above sequence of creation messages is very common,
the ScrollableView
class provides a more convenient
instance creation message; instead of:
...
v := ScrollableView new.
t := TextView new.
v scrolledView:t.
...
you can also write:
...
v := ScrollableView for:TextView.
...
A scrolledView forwards all unimplemented messages to its scrolledView,
therefore, you can often omit the access to its scrolledView, and write:
|v|
v := ScrollableView for:TextView.
v contents:('/etc/hosts' asFilename readStream contents).
v open
By the way: the following is probably the shortest code to set up an editor:
(we will come to the Dialog class soon ...)
If you want to have your own view scrolled, use the following code:
|fileName v top|
fileName := Dialog requestFileName:'edit which file'.
(fileName notNil and:[fileName asFilename exists]) ifTrue:[
top := StandardSystemView new.
top label:'editing ' , fileName.
v := ScrollableView for:EditTextView in:top.
v origin:0.0 @ 0.0 corner:1.0 @ 1.0.
v contents:(fileName asFilename readStream contents).
top open
]
|top v myView|
....
top := StandardSystemView new ....
...
v := ScrollableView origin:0.0 @ 0.0
corner:1.0 @ 1.0
in:top.
...
myView := MyViewClass new.
v scrolledView:myView
...
top open
...
your view (an instance of 'MyViewClass') will be asked for the size and position of
the contents (so that the scroller can reflect this correctly) by the following
messages:
Whenever moved, the scrollbar will ask your view to scroll accordingly.
This is done by sending it messages like scrollVerticalTo:
.
However, since there is a reasonable default implementation of all these
scroll methods (in the View
class),
there is normally no need to add code
for scrolling support in subclasses of view.
If you need your view to be scrollable both vertically and
horizontally, use HVScrollableView
instead:
|v|
v := HVScrollableView for:TextView.
v contents:('/etc/hosts' asFilename readStream contents).
v extent:300@200.
v open
ScrollableView
and HVScrollableView
can be configured to use miniScrollers instead of full size scrollBars.
|v|
v := HVScrollableView for:TextView miniScrollerH:true miniScrollerV:false.
v contents:('/etc/hosts' asFilename readStream contents).
v extent:300@200.
v open
or, if vertical scrolling is the uncommon case (this really depends
on your application !), use:
|v|
v := HVScrollableView for:TextView miniScrollerH:false miniScrollerV:true.
v contents:('/etc/hosts' asFilename readStream contents).
v extent:300@200.
v open
of course, it is also possible to use miniscrollers for both directions:
|v|
v := HVScrollableView for:TextView miniScrollerH:true miniScrollerV:true.
v contents:('/etc/hosts' asFilename readStream contents).
v extent:300@200.
v open
As a concrete example, the current ST/X system uses miniscrollers
for the selectionInListViews of the systemBrowser -
since these are rarely scrolled horizontally.
ListView
.
This does not offer any editing or selection capabilities,
and is therefore normally not used directly in the system.
However, many subclasses inherit the functionality
directly or indirectly from ListView
,
using this class as a framework.
ListViews
main purpose is to handle all redrawing and scrolling;
while the subclasses add user interaction.
Despite that, it is useful to create a listView and set its contents
for didactic purposes -
the protocol of the other text view classes is either directly implemented
by ListView
, or redefined.
Therefore, learning more about listViews protocol is quite useful.
#list:
message,
passing a (sequenceable) collection of strings as argument.
#contents:
message,
which expects a string argument. This method will break the string
into lines (taking cr
as separator), expand tabulators into
spaces and set the list from the resulting collection.
|top listView|
top := StandardSystemView label:'a simple listview'.
top extent:200@200.
listView := ListView in:top.
listView origin:0.0 @ 0.0 corner:1.0 @ 1.0.
listView list:#('one' 'two' 'three' nil 'five' 'six').
top open
or, passing a string:
|top listView|
top := StandardSystemView label:'a simple listview'.
top extent:200@200.
listView := ListView in:top.
listView origin:0.0 @ 0.0 corner:1.0 @ 1.0.
listView contents:'one
two
three
five
six'.
top open
or, reading the text from a file:
|top listView|
top := StandardSystemView label:'a simple listview'.
top extent:200@200.
listView := ListView in:top.
listView origin:0.0 @ 0.0 corner:1.0 @ 1.0.
listView contents:('/etc/hosts' asFilename contentsOfEntireFile).
top open
Normally, you want to the listView to be scrollable.
Compare the non scrollable case:
|top listView|
top := StandardSystemView label:'a simple listview'.
top extent:300@400.
listView := ListView in:top.
listView origin:0.0 @ 0.0 corner:1.0 @ 1.0.
listView contents:('/etc/hosts' asFilename contentsOfEntireFile).
top open
with the scrollable case:
|top scrollView listView|
top := StandardSystemView label:'a scrollable listview'.
top extent:300@400.
scrollView := ScrollableView for:ListView in:top.
listView := scrollView scrolledView.
scrollView origin:0.0 @ 0.0 corner:1.0 @ 1.0.
listView contents:('/etc/hosts' asFilename contentsOfEntireFile).
top open
or scrollable in both directions:
|top scrollView listView|
top := StandardSystemView label:'a scrollable listview'.
top extent:300@400.
scrollView := HVScrollableView for:ListView miniScrollerH:true in:top.
listView := scrollView scrolledView.
scrollView origin:0.0 @ 0.0 corner:1.0 @ 1.0.
listView contents:('/etc/hosts' asFilename readStream contents).
top open
As you will notice, listViews do not support selections, editing or even
a popup menu - we will see how this is done in the next section..
You can ask a listView about its contents, either via the #contents
message (which returns a string) or the #list
message
(which returns the list). Be careful when changing the list - you will get
a reference to its internal list - not a copy. The string returned by
#contents
is always constructed anew - changing it will not
affect the listViews contents.
To change the list AND have the listView redisplay the changed lines, either
#list:
or #contents:
message,
passing the modified text,
#at:put:
, #add:
, #add:beforeIndex:
and #removeIndex:
).
Using those access messages (instead of setting the complete list) has the advantage that the listView can optimize its redraw operations; therefore, these are typically a bit faster - especially, if your display connection is a slow one - and avoid visible flicker.
If you set the complete list via #list:
, the listView
changes the scroll position to the top of the text
- if this is not desired, use #setList:
,
which lets the scroll position remain unchanged.
(the fileBrowser uses this, to update the list when the list of files
in the directory has changed).
All of the above described the direct list access protocol, using direct interaction of the changer with the listView. This setup is convenient for simple setups, where a one-to-one relation between the text and the view exists.
ListView
can also be used with a model, which holds the
actual text. Here the interaction is indirect: the changes are performed
to the model, which informs the listView - or, possibly - multiple listViews.
If a model is set in a listView, it should send out
change notifications ("aModel changed:aspect"
) to
have the listView update its contents.
To acquire a new text, the listView sends a listMessage back
to the model - which should return the new text.
The listMessage defaults to the aspect selector, but it can
be changed with listMessage:
to any other message selector (independent of the aspect).
Example using a model:
Although, the normal setup (listMessage == aspectMessage) is ok for
most applications, there are situations, where it makes sense to
have multiple listViews access the text via different messages from the
same model.
|top model l theModelsText|
"/ the model is normally one of your classes ...
model := Plug new.
model respondTo:#modelsAspect
with:[ theModelsText ].
top := StandardSystemView new.
top extent:100@200.
l := ListView origin:0.0 @ 0.0 corner:1.0 @ 1.0 in:top.
l model:model.
l listMessage:#modelsAspect.
l aspect:#modelsAspect.
top open.
Delay waitForSeconds:3.
theModelsText := #('foo' 'bar' 'baz').
model changed:#modelsAspect.
Delay waitForSeconds:1.
theModelsText := #('foo' 'bar' 'baz' 'nice - isn''t it').
model changed:#modelsAspect.
ScrollableView
which does things for you.
However, it is sometimes useful to scroll the text automatically for the user's convenience. For example, in an error-log view, you may want to scroll to the end, or in a fileList, it is useful to scroll to the position of the previously selected file.
The low level entries to scrolling are:
scrollUp
/ scrollDown
scrollUp:
n / scrollDown:
n
scrollLeft
/ scrollRight
scrollLeft:
n / scrollRight:
n
scrollToLine:
nr / scrollToCol:
nr
scrollToTop
/ scrollToBottom
scrollToLeft
scrollHorizontalToPercent:
p / scrollVerticalToPercent:
p
pageUp
/ pageDown
halfPageUp
/ halfPageDown
makeLineVisible:
nr
makeColVisible:
nr
makeVisible:
string
ListView
, a TextView
knows how to
deal with a collection of text lines. In addition, it supports
a text selection, and provides the required user interaction.
TextView
do not support editing - thus, they are
only useful for readonly text. For red/write texts,
see the EditTextView
description
below.
Programmatically, the selection can be changed with:
selectAll
selectLine:
lineNr
selectFromLine:
startLine toLine:
endLine
selectFromLine:
startLine col:
startCol toLine:
endLine col:
endCol
selectWordAtLine:
lineNr col:
colNr
selectWordAtLine:
lineNr col:
colNr
unselect
hasSelection
selection
#asString
to the returned
object ... (if it's nonNil).
selectionStartLine
selectionEndLine
selectionStartCol
selectionEndCol
|top textView|
top := StandardSystemView extent:400@300.
textView := TextView origin:0.0@0.0 corner:1.0@1.0 in:top.
textView contents:('/etc/hosts' asFilename contentsOfEntireFile).
top open.
Example (scrollable):
|top textView|
top := StandardSystemView extent:400@300.
textView := ScrollableView for:TextView in:top.
textView origin:0.0 @ 0.0 corner:1.0 @ 1.0.
textView contents:('/etc/hosts' asFilename contentsOfEntireFile).
top open.
EditTextView
is similar to TextView
,
with additional support for editing operations and a textCursor.
EditTextView
offers a callBack
for the accept operation - either as a block (the acceptAction) or,
by sending a change notification to the views model (if there is a non-nil model).
cursorLine:
lineNr col:
colNr
cursorHome
cursorToBottom
#acceptAction:
) will be evaluated,
and gets the views contents (a collection of lines) as argument.
#modified:
message.
|top textView|
top := StandardSystemView extent:400@300.
textView := EditTextView origin:0.0@0.0 corner:1.0@1.0 in:top.
textView acceptAction:[:contents |
textView modified ifTrue:[
Transcript showCR:'*** saving:'.
Transcript cr.
Transcript showCR:contents asString.
textView modified:false
] ifFalse:[
Transcript showCR:'*** no save - not modified since last save'
]
].
top open.
TextCollector
is similar to EditTextView
,
with additional support for streaming operations.
|top textView nooTop nooOutputStream|
nooTop := StandardSystemView extent:400@400 label:'noo output stream'.
nooOutputStream := TextCollector origin:0.0@0.0 corner:1.0@1.0 in:nooTop.
nooTop open.
top := StandardSystemView extent:400@300.
textView := EditTextView origin:0.0@0.0 corner:1.0@1.0 in:top.
textView acceptAction:[:contents |
textView modified ifTrue:[
nooOutputStream showCR:'*** saving:'.
nooOutputStream cr.
nooOutputStream showCR:contents asString.
textView modified:false
] ifFalse:[
nooOutputStream showCR:'*** no save - not modified since last save'
]
].
top open.
|top textView nooTop nooOutputStream|
nooTop := StandardSystemView extent:400@400 label:'noo output stream'.
nooOutputStream := Workspace origin:0.0@0.0 corner:1.0@1.0 in:nooTop.
nooTop open.
nooOutputStream showCR:' |top b|'.
nooOutputStream showCR:' top := StandardSystemView label:''a button''.'.
nooOutputStream showCR:' top extent:100@100.'.
nooOutputStream showCR:' b := Button in:top.'.
nooOutputStream showCR:' b label:''press me''.'.
nooOutputStream showCR:' b origin:0.1@0.1 corner:0.9@0.9.'.
nooOutputStream showCR:' b action:[Transcript showCR:''hello there''].'.
nooOutputStream showCR:' top open'.
|top textView nooTop nooOutputStream|
nooTop := StandardSystemView extent:400@400 label:'noo output stream'.
nooOutputStream := CodeView origin:0.0@0.0 corner:1.0@1.0 in:nooTop.
nooTop open.
nooOutputStream showCR:' |top b|'.
nooOutputStream showCR:' top := StandardSystemView label:''a button''.'.
nooOutputStream showCR:' top extent:100@100.'.
nooOutputStream showCR:' b := Button in:top.'.
nooOutputStream showCR:' b label:''press me''.'.
nooOutputStream showCR:' b origin:0.1@0.1 corner:0.9@0.9.'.
nooOutputStream showCR:' b action:[Transcript showCR:''hello there''].'.
nooOutputStream showCR:' top open'.
SelectionInListView
is a ListView
with a selected line, which is shown highlighted.
|top slv|
top := StandardSystemView new
label:'select';
minExtent:100@100;
maxExtent:300@400;
extent:200@200.
slv := SelectionInListView new.
slv list:#('one' 'two' 'three').
slv action:[:index | Transcript showCr:'selected ' , index printString].
top add:slv in:(0.0@0.0 corner:1.0@1.0).
top open
If toggleSelect is true, clicking toggles the selection, i.e. click on a seleted item will deselect the item.
|top slv|
top := StandardSystemView new
label:'select';
minExtent:100@100;
maxExtent:300@400;
extent:200@200.
slv := SelectionInListView new.
slv toggleSelect:true.
slv list:#('one' 'two' 'three').
slv selectElement:'one'.
slv action:[:index | Transcript showCr:'selected ' , index printString , ' ', slv selectionValue printString].
top add:slv in:(0.0@0.0 corner:1.0@1.0).
top open.
Note that the selectElement method selects an initial value.
Note also that index can also be used as in the following example.
|top slv|
top := StandardSystemView new
label:'select';
minExtent:100@100;
maxExtent:300@400;
extent:150@150.
slv := SelectionInListView new.
slv toggleSelect:true; useIndex:false.
slv list:#('one' 'two' 'three' 'four' 'five' 'six' 'seven' 'eight' 'nine' 'ten' 'eleven' 'twelve' 'thirteen').
slv selection:6.
slv action:[:element | Transcript showCr:'selected ' , element printString].
top add:(ScrollableView forView:slv) in:(0.0@0.0 corner:1.0@1.0).
top open
Note that using the index is advantageous if there are multiple instances of the same element.
|top slv|
top := StandardSystemView new
label:'select';
minExtent:100@100;
maxExtent:300@400;
extent:200@200.
slv := SelectionInListView new.
slv toggleSelect:true; useIndex:false; multipleSelectOk:true.
slv list:#('one' 'two' 'three' 'four' 'five'
'six' 'seven' 'eight' 'nine' 'ten'
'eleven' 'twelve' 'thirteen').
slv action:[:element |
Transcript showCr:'selected ' , element printString
].
slv makeSelectionVisible.
top add:(ScrollableView forView:slv) in:(0.0@0.0 corner:1.0@1.0).
top open
FileSelectionList
is a file selection list.
It is basically a SelectionInListView
with some extra touches for directory selections,
for example, a matching pattern for filtering out unwanted files, a arrow mark for indicating directories,
an action block for doing something with the chosen directory,
and other little tidbits to be seen in the following examples.
|list|
list := FileSelectionList new.
list open
"That wasn't bad. But a scroll bar would be nice, and showing the chosen file in the
transcript would also be advantageous."
|top v list|
top := StandardSystemView new.
top extent:(300 @ 200).
v := ScrollableView for:FileSelectionList in:top.
v origin:(0.0 @ 0.0) corner:(1.0 @ 1.0).
list := v scrolledView.
list action:[:index |
Transcript showCr:'you selected: ' , list selectionValue
].
top open
|top v list|
top := StandardSystemView new.
top extent:(300 @ 200).
v := ScrollableView for:FileSelectionList in:top.
v origin:(0.0 @ 0.0) corner:(1.0 @ 1.0).
list := v scrolledView.
list ignoreDirectories:true.
top open
"Or instead, let's just not show the directories as such:"
|top v list|
top := StandardSystemView new.
top extent:(300 @ 200).
v := ScrollableView for:FileSelectionList in:top.
v origin:(0.0 @ 0.0) corner:(1.0 @ 1.0).
list := v scrolledView.
list markDirectories:false.
top open
"Next a filter can be set using the pattern method:"
|top v list|
top := StandardSystemView new.
top extent:(300 @ 200).
v := ScrollableView for:FileSelectionList in:top.
v origin:(0.0 @ 0.0) corner:(1.0 @ 1.0).
list := v scrolledView.
list pattern:'*.st'.
list action:[:index |
Transcript showCr:'you selected: ' , list selectionValue
].
top open
|top v list|
top := StandardSystemView new.
top extent:(300 @ 200).
v := ScrollableView for:FileSelectionList in:top.
v origin:(0.0 @ 0.0) corner:(1.0 @ 1.0).
list := v scrolledView.
list matchBlock:[:name |
|fileName|
fileName := name asFilename.
fileName isWritable or:[fileName isDirectory]
].
list action:[:index | Transcript showCr:'you selected: ' , list selectionValue].
top open
|b|
b := InfoBox new.
b title:'how about this ?'.
b show
Notice:
it can also be opened using#open
or#openModal
(even#openModeless
if you like). However,#show
has been added to allow easier search on DialogBoxes using the browser - if all your DialogBoxes are opened with#show
, you will find all places where modal boxes are used, by looking for senders of 'show*'. If you used#open
, you still have to look at the code to decide if its a modalBox or regular view.)
Using #show
, the box will open-up at some unspecified place on the screen.
Actually, it is either its default position, or the position where it
was opened previously. This is almost always not the place, where you want
the box to appear.
Thus, you should either set its origin, as in:
or:
|b|
b := InfoBox new.
b title:'how about this ?'.
b origin:0@0.
b show
or, more convenient for the user, with:
|b|
b := InfoBox new.
b title:'how about this ?'.
b origin:(Display extent - b extent).
b show
here, the box will showup whereever the mouse-pointer is currently located.
You should always use this to open your boxes, since it is more convenient
for the person behind the glass (no mouse movement is needed for confirmation).
|b|
b := InfoBox new.
b title:'how about this ?'.
b showAtPointer
If there is some other view, which you do not want to cover (usually the view which launched the box), you should use:
b showAtPointerNotCovering:anotherView
(where anotherView is typically 'self' in your program).
Using this, the box will show itself either to the right or left of the
specified view.
The following example looks more complicated than needed, since in this
immediate doit-evaluation, there is no self available:
Finally, for very urgent information, use:
|b|
b := InfoBox new.
b title:'how about this ?'.
b showAtPointerNotCovering:(WindowGroup activeGroup topViews first)
This will open the box at the center of the screen.
|b|
b := InfoBox new.
b title:'water in disk !!!!'.
b showAtCenter
By the way: there is a shortcut available for creating AND setting the title
of the box:
Also, every object understands another shortcut message:
(InfoBox title:'wow !') showAtPointer
#information:
.
Which takes the argument as a titletext and opens an info box for it.
Thus you can use:
everywhere in your program.
self information:'this''s simple'
The reason for telling you about all the individual messages is that they
allow more customized boxes to be set up. The easy-to-use box shown with
the #information
message is only a very general box.
For example:
or:
|b|
b := InfoBox new.
b title:'this operation will flood your harddisk ?'.
b okText:'are you certain ?'.
b okButton enteredForegroundColor:(Color red).
b formLabel foregroundColor:(Color red).
b textLabel foregroundColor:(Color blue).
b showAtPointer
If you plan to use customized boxes as the above, it may be a good idea to
create a subclass of WarnBox (say 'OutOfPaperBox') for the above - this makes
certain, that all boxes look alike, saves code by not replicating this setup
everywhere, and finally makes your program easier to maintain, since there is
only one place you have to modify, in case changes have to be made.
|b|
b := InfoBox new.
b okText:'ok, refilled '.
b okButton enteredBackgroundColor:(Color red lightened).
b title:'Your printer is out of paper !\\please refill before continuing' withCRs.
b image:(Image fromFile:'goodies/bitmaps/xpmBitmaps/device_images/ljet.xpm').
b formLabel level:1.
b textLabel foregroundColor:(Color red).
b showAtPointer
i.e. (suggestion):
InfoBox subclass:#OutOfPaperBox
instanceVariableNames:''
classVariableNames:''
poolDictionaries:''
category:'MyViews-DialogBoxes'
!OutOfPaperBox class methodsFor:'instance creation'!
new
|b|
b := super new.
b okText:'ok, refilled '.
b okButton enteredBackgroundColor:(Color red lightened).
b title:'Your printer is out of paper !!\\please refill before continuing' withCRs.
b image:(Image fromFile:'goodies/bitmaps/xpmBitmaps/device_images/ljet.xpm').
b formLabel level:1.
b textLabel foregroundColor:(Color red).
^ b
! !
you can then show those boxes with:
OutOfPaperBox new showAtPointer
(InfoBox title:'wow !') showAtPointer
and:
(WarningBox title:'wow !') showAtPointer
Also, WarningBoxes beep when coming up, while InfoBoxes are silent.
Since warnings are also very common, there is a convenient message to create
these:
self warn:'something is wierd'
WarningBoxes inherit from InfoBox. Therefore all messages in InfoBox to access
or modify their appearance can also be applied to them.
|b result|
b := YesNoBox new.
b title:'do you like ST/X ?'.
b yesAction:[result := true].
b noAction:[result := false].
b showAtPointer.
self information:('the result was: ' , result printString).
There are all kinds of things you can change in the look of the box. We will
only look at a few things that are possible. For further information look into
the box-classes with the browser.
|b|
b := YesNoBox new.
b title:'something else'.
b textLabel font:(Font family:'times' face:'bold' style:'roman' size:18).
b okText:'wow great'.
b noText:'mhmh'.
b yesButton foregroundColor:(Color green).
b image:(Image fromFile:'libtool/bitmaps/SmalltalkX.xbm').
b yesAction:[Transcript showCR:'yes was pressed'].
b noAction:[Transcript showCR:'no was pressed'].
b showAtPointer
Also, if you are simply interested in the result of a simple yes/no question,
you can open the box with the confirm-message instead of defining the
action blocks:
|b result|
b := YesNoBox new.
result := b confirm:'are you sure ?'.
Transcript showCR:('the answer is ' , result printString)
In the above example, you really do not need the temporary variable 'b'.
Thus, the same can be done more condensed with:
|result|
result := YesNoBox new confirm:'are you sure ?'.
Transcript showCR:('the answer is ' , result printString)
if you are asking multiple questions, the box can be reused, as in:
|b result|
b := YesNoBox new.
result := b confirm:'are you sure ?'.
result ifTrue:[
result := b confirm:'definitely ?'.
result ifTrue:[
result := b confirm:'absolutely certain ?'.
result ifTrue:[
Transcript showCR:'ok'
]
]
]
Since this kind of confirmation is also very common, there is a convenient
shortcut too:
|result|
result := self confirm:'answer yes or no'
This returns either true or false, depending on which button the user has pressed.
Since #confirm:
is defined in Object
, every receiver can be used for the #confirm:
message above (it works for every self).
|b|
b := EnterBox new.
b title:'enter your name, please'.
b initialText:(OperatingSystem getLoginName).
b action:[:theString | Transcript showCR:'the name is ' , theString].
b showAtPointer.
The box does not evaluate the action-block if cancel is pressed.
Therefore you should be prepared for this, in your program:
|b value|
value := nil.
b := EnterBox new.
b title:'enter your name, please'.
b initialText:(OperatingSystem getLoginName).
b action:[:theString | value := theString].
b showAtPointer.
value isNil ifTrue:[
Transcript showCR:'operation cancelled'
] ifFalse:[
Transcript showCR:'operation to be performed with ' , value
]
Since it is sometimes a bit inconvenient, to setup a box and define
all those actions, there are some standard messages prepared for the
most common queries. These are defined as class-messages of EnterBox
and
YesNoBox
(for compatibility with ST-80,
there are additional classes called
DialogBox
and Dialog
which also understand these).
|b value|
b := EnterBox new.
b title:'enter your name, please'.
b initialText:(OperatingSystem getLoginName).
value := b request:'enter your name, please'.
value isNil ifTrue:[
Transcript showCR:'operation cancelled'
] ifFalse:[
Transcript showCR:'operation to be performed with ' , value
]
Even more compact code is possible using class messages:
|result|
result := EnterBox request:'enter some string'.
Transcript showCR:result.
or use the ST-80 compatible:
|result|
result := Dialog request:'enter some string'.
Transcript showCR:result.
Have a look at DialogView
for more on this.
|b|
b := EnterBox2 new.
b title:'enter a fileName, please'.
b initialText:'newFile'.
b okText:'append'.
b action:[:name | Transcript showCR:'you want to append to ' , name].
b okText2:'save'.
b action2:[:name | Transcript showCR:'you want to save to ' , name].
b showAtPointer
EnterBox
and
EnterBox2
have been
provided.
A convenient interface (which sets up the box to return a value)
is:
The returned value could be used with #perform for some real action.
|what|
what := OptionBox
request:'what do you want to do ?'
label:'Attention'
image:(WarningBox iconBitmap)
buttonLabels:#('abort' 'accept' 'continue')
values:#(#abort #accept #continue).
Transcript showCR:'you selected: ' , what.
|box|
box := ListSelectionBox new.
box title:'which color'.
box list:#('red' 'green' 'blue' 'white' 'black').
box action:[:aString | Transcript showCR:'selected: ' , aString].
box showAtPointer
You can also preset an initial string:
|box|
box := ListSelectionBox new.
box title:'which color'.
box list:#('red' 'green' 'blue' 'white' 'black').
box action:[:aString | Transcript showCR:'selected: ' , aString].
box initialText:'fooBar'.
box showAtPointer
FileSelectionBox
looks like a ListSelectionBox
,
but the list consists of the file names in a directory.
|box|
box := FileSelectionBox new.
box title:'which file ?'.
box action:[:aString | Transcript showCR:'selected: ' , aString].
box showAtPointer
You can also specify the directory:
|box|
box := FileSelectionBox new.
box directory:'/usr'.
box title:'which file ?'.
box action:[:aString | Transcript showCR:'selected: ' , aString].
box showAtPointer
and/or a filename-pattern:
|box|
box := FileSelectionBox new.
box pattern:'*.st'.
box directory:'../../libbasic'.
box title:'which file ?'.
box action:[:aString | Transcript showCR:'selected: ' , aString].
box showAtPointer
and/or a filterBlock to select which filenames are shown:
|box|
box := FileSelectionBox new.
box pattern:'*.st'.
box matchBlock:[:fileName | fileName first between:$A and:$F].
box directory:'../../libbasic'.
box title:'which file ?'.
box action:[:aString | Transcript showCR:'selected: ' , aString].
box showAtPointer
the box remembers its last directory and filename. Therefore, you may
reuse the old box in your application instead of recreating new ones.
To see how this works, evaluate the following code, then change the
directory in the first box and press ok.
The second box will show up with the last directory:
The FileSelectionBox uses an instance of
|box|
box := FileSelectionBox new.
box title:'which file ?'.
box action:[:aString | Transcript showCR:'selected: ' , aString].
box showAtPointer.
box title:'again - which file ?'.
box action:[:aString | Transcript showCR:'selected2: ' , aString].
box showAtPointer
FileSelectionList
to show the fileNames and handle selection.
That class offers alot of options to control which files are shown
(matchBlocks & name patterns). Also, it is possible to disable
a change into other directories or to hide either regular files
or directories completely.
For most standard queries, convenient class methods are available
(for compatibility: in the Dialog
class.
If you simply want to query for a fileName, use something such as:
or:
|fileName|
fileName := Dialog requestFileName:'which file ?'.
Transcript showCR:'the name is: ' , fileName.
To ask for a directory name, use:
|fileName|
fileName := Dialog requestFileName:'which file ?' default:'foo'.
Transcript showCR:'the name is: ' , fileName.
There are a few more of these request methods available - see
the
|fileName|
fileName := Dialog requestDirectoryName:'which directory ?'.
Transcript showCR:'the name is: ' , fileName.
DialogBoxes
class protocol.
appendAction:
|box|
box := FileSaveBox new.
box title:'which file ?'.
box action:[:aString | Transcript showCR:'save to: ' , aString].
box appendAction:[:aString | Transcript showCR:'append to: ' , aString].
box showAtPointer
|box|
box := FontPanel new.
box action:[:aFont | Transcript showCR:'font is: ' , aFont].
box showAtPointer
DialogBox
.
However, for compatibility with ST-80,
dialogBox offers class methods which create and show these dialogs.
Dialog
for compatibility with future versions
and with other smalltalk systems.
Dialog information:'hello there'.
for warnings:
Dialog warn:'oops - something happened'.
for simple boolean questions (returns true or false):
|answer|
answer := Dialog confirm:'yes or no ?'.
with cancel (if cancelled, the returned value is nil):
|answer|
answer := Dialog confirmWithCancel:'yes or no ?'.
multiple choice entry (returns corresponding entry from values arg):
|answer|
answer := Dialog
choose:'choose any'
labels:#('one' 'two' 'three' 'four')
values:#(1 2 3 4)
default:2
to ask for a string (returns nil, if cancelled):
|answer|
answer := Dialog request:'enter your name here:'.
as above, with an initial string:
|answer|
answer := Dialog request:'enter your name here:'
initialAnswer:'foo-user'.
for password entry, the typed input is invisible in:
|answer|
answer := Dialog requestPassword:'enter secret code:'
to enter a fileName (returns nil if cancelled):
|answer|
answer := Dialog requestFileName:'enter a filename here:'
default:'newFile'.
Text about how custom dialogs are created using the Dialog class to be added here. For now, see examples in DialogBox's documentation category.
|aMenu|
aMenu := PopUpMenu
labels:#('foo' 'bar')
selectors:#(doFoo doBar)
receiver:someObject
the menu is shown with:
aMenu showAtPointer
When activated, the menu will send a #doFoo
or #doBar
message to someObject.
In some situations, it is more convenient to use the same selector
for all menu entries and provide different arguments.
This can be done by creating the menu via:
|aMenu|
aMenu := PopUpMenu
labels:#('foo' 'bar')
selector:#someSelector:
args:#( 'argForFoo' 'argForBar')
receiver:someObject
When activated, the menu will send #someSelector:'argForFoo'
and #someSelector:'argForBar'
respectively.
Finally, the most general setup is by defining individual selector/argument combinations for every menu entry:
|aMenu|
aMenu := PopUpMenu
labels:#('foo' 'bar' 'baz')
selectors:#(doFoo: doBar doBaz:)
args:#( 'argForFoo' nil 'argForBaz')
receiver:someObject
#middleButtonMenu:
message with the menu as an argument.
Try the following:
(if you try this example, be prepared to have a debugger come up - the view
will of course not understand any foo-bar messages.
|myView myMenu|
myView := View new.
myMenu := PopUpMenu
labels:#('foo' 'bar')
selectors:#(doFoo doBar)
receiver:myView.
myView middleButtonMenu:myMenu.
myView open
Simply press 'continue' or 'abort' to leave the debugger)
For a working example, try:
This example also shows how grouping lines are added to a menu.
The item labels
|v m|
v := View new.
m := PopUpMenu
labels:#('lower'
'raise'
'-'
'destroy')
selectors:#(#lower #raise nil #destroy)
receiver:v.
v middleButtonMenu:m.
v open
'-'
and '='
are
drawn as single and double separating lines respectively.
If (which is unlikely) you need these item labels, use '\-'
and '\='
respectively.
(For ST-80 compatibility, menus can also be created by passing the line
information in an extra argument, as described below.)
Sometimes, you want to specify both selectors and arguments
to be sent; this is done by:
you may mix selectors for methods with and without arguments;
but only 0 (zero) or 1 (one) argument selectors are allowed:
|v m|
v := View new.
m := PopUpMenu
labels:#('foo' 'bar' 'baz')
selectors:#(#foo: #bar: #foo:)
args:#(1 2 3)
receiver:nil.
v middleButtonMenu:m.
v open.
or, have the menu send the same selector but pass different arguments:
|v m|
v := View new.
m := PopUpMenu
labels:#('foo' 'bar' 'baz')
selectors:#(#foo: #bar #foo:)
args:#(1 'ignored' 3)
receiver:nil.
v middleButtonMenu:m.
v open.
|v m|
v := View new.
m := PopUpMenu
labels:#('foo' 'bar' 'baz')
selectors:#foo:
args:#(1 2 3)
receiver:nil.
v middleButtonMenu:m.
v open.
'\c'
(for check-mark).
The value passed with the items message will be the truth state of the
check-mark (i.e. true or false).
|m v|
v := View new.
m := PopUpMenu
labels:#('\c foo'
'\c bar')
selectors:#(#value: #value:)
receiver:[:v | Transcript show:'arg: '; showCR:v].
v middleButtonMenu:m.
v open
Currently, three different types of checkmarks are supported,
choosen by different special sequences:
'\c'
- simple check mark
'\b'
- a check box
'\t'
- a thumbsUp/thumbsDown check mark
|m v|
v := View new.
m := PopUpMenu
labels:#('\c foo'
'\c bar'
'-'
'\b more foo'
'\b more bar'
'='
'\t all right'
)
selectors:#value:
receiver:[:v | Transcript show:'arg: '; showCR:v].
v middleButtonMenu:m.
v open
The current scheme is somewhat limited, in only providing
3 different (and hardcoded) check mark types.
Future versions may provide more flexible checkmarks.
#hideSubmenus
, #deselectWithoutRedraw
and others), see
the MenuView
classes protocol or have a look at the
implementation of the PatternMenu
class,
or just try and see where you reach the debugger ;-).
For the curious: this wrappability is the reason for the somewhat complicated
separation of menus into a PopUpMenu
class which
handles the basic popping and a MenuView
class, which
actually displays the menu.
Currently there is only one class in the system,
which is prepared and can be used
this way (PatternMenu
in the DrawTool demo).
PatternMenu
has been declared
as subclass of MenuView
- so it automatically understands all these messages.
or try:
(have a careful look at the receiver of the menu-message ;-)
|v p|
v := View new.
p := PatternMenu new.
p patterns:(Array with:Color red
with:Color green
with:Color blue).
v middleButtonMenu:(PopUpMenu forMenu:p).
v open
or even (see below for more on submenus):
|v p|
v := View new.
p := PatternMenu new.
p patterns:(Array with:Color red
with:Color green
with:Color blue).
p selectors:#value:.
p receiver:[:val | v viewBackground:val. v clear].
p args:(Array with:Color red
with:Color green
with:Color blue).
v middleButtonMenu:(PopUpMenu forMenu:p).
v open
You will find some more examples in the PatternMenus class documentation.
|v pMain pRed pGreen pBlue colors|
v := View new.
pMain := PatternMenu new.
pMain patterns:(Array with:Color red
with:Color green
with:Color blue).
pMain selectors:#(red green blue).
pRed := PatternMenu new.
colors := (Array with:(Color red:100 green:0 blue:0)
with:(Color red:75 green:0 blue:0)
with:(Color red:50 green:0 blue:0)
with:(Color red:25 green:0 blue:0)).
pRed patterns:colors.
pRed selectors:#value:.
pRed args:colors.
pRed receiver:[:val | v viewBackground:val. v clear].
pRed windowRatio:(4 @ 1).
pMain subMenuAt:#red put:(PopUpMenu forMenu:pRed).
pGreen := PatternMenu new.
colors := (Array with:(Color red:0 green:100 blue:0)
with:(Color red:0 green:75 blue:0)
with:(Color red:0 green:50 blue:0)
with:(Color red:0 green:25 blue:0)).
pGreen patterns:colors.
pGreen selectors:#value:.
pGreen args:colors.
pGreen receiver:[:val | v viewBackground:val. v clear].
pGreen windowRatio:(2 @ 2).
pMain subMenuAt:#green put:(PopUpMenu forMenu:pGreen).
pBlue := PatternMenu new.
colors := (Array with:(Color red:0 green:0 blue:100)
with:(Color red:0 green:0 blue:75)
with:(Color red:0 green:0 blue:50)
with:(Color red:0 green:0 blue:25)).
pBlue patterns:colors.
pBlue selectors:#value:.
pBlue args:colors.
pBlue receiver:[:val | v viewBackground:val. v clear].
pBlue windowRatio:(1 @ 4).
pMain subMenuAt:#blue put:(PopUpMenu forMenu:pBlue).
v middleButtonMenu:(PopUpMenu forMenu:pMain).
v open
Static menus can be used with any other view
- the following adds one to a button:
The buttons left-mouse-button functionality is not affected by the
added middle-button menu.
|top b|
top := StandardSystemView label:'a button'.
top extent:100@100.
b := Button in:top.
b label:'press me'.
b origin:0.1@0.1 corner:0.9@0.9.
b middleButtonMenu:(PopUpMenu labels:#('foo' 'bar')).
top open
BTW: this is how PopUpList is implemented.
|m selection|
m := PopUpMenu
labels:#('one' 'two' 'three').
selection := m startUp.
Transcript show:'the selection was: '; showCR:selection
startUp will return the entries index, or 0 if there was no selection.
You can also specify an array of values to be returned instead of the
index:
|m selection|
m := PopUpMenu
labels:#('one' 'two' 'three')
values:#(10 20 30).
selection := m startUp.
Transcript show:'the value was: '; showCR:selection
In ST/X style menus, separating lines between entries are created
by a '-'
-string as its label text (and corresponding nil-entries in the
selectors- and args-arrays).
In ST-80, you have to pass the indices of the lines in an extra array:
|m selection|
m := PopUpMenu
labels:#('one' 'two' 'three' 'four' 'five')
lines:#(2 4).
selection := m startUp.
Transcript show:'the value was: '; showCR:selection
or:
|m selection|
m := PopUpMenu
labels:#('one' 'two' 'three')
lines:#(2)
values:#(10 20 30).
selection := m startUp.
Transcript show:'the value was: '; showCR:selection
Use whichever interface (ST-80 or ST/X) you prefer.
#subMenuAt:put:
.
|v main sub|
v := View new.
main := PopUpMenu
labels:#('foo' 'bar' '-' 'more')
selectors:#(#foo #bar nil #more)
receiver:nil.
sub := PopUpMenu
labels:#('more foo' 'more bar')
selectors:#(moreFoo moreBar)
receiver:nil.
main subMenuAt:#more put:sub.
v middleButtonMenu:main.
v open.
The index (first argument) to the subMenuAt:put:
message may
be either an entry label-text, a numeric index starting at 1,
or the selector. Please use the selector, since the string could
be different for national variants.
Also the numeric index may change as your menu gets more indices.
(see below on how to dynamically add/remove entries).
aMenu indexOf:someKey
where key can be a selector or an entries text (again use the selector).
aMenu addLabel:'something' selector:#foo after:anIndex
or, to add a submenu:
aMenu addLabel:'something' selector:#foo after:anIndex
aMenu subMenuAt:(anIndex + 1) put:aNewSubmenu.
In analogy, entries are removed with:
aMenu remove:someIndex
where index is again, either numeric, a selector or an entries text.
Finally, you can change both label and selector of entries:
aMenu labelAt:index put:'newLabel'
and:
aMenu selectorAt:index put:#fooBar
Lets wrap all this into an example:
(I use a block as the receiver here since in this doIt-example,
there is no class to implement the messages sent from the menu.
In real programs, the receiver is either some view or model.
The message sent is then some action-methods message, instead of #value:
)
|v menu action|
action := [:action |
action == #add ifTrue:[
menu addLabel:'newLabel'
selector:#newFunction
after:2
].
action == #remove ifTrue:[
menu remove:#newFunction
]
].
v := View new.
menu := PopUpMenu
labels:#('add' 'remove')
selectors:#(#value: #value:)
args:#(add remove)
receiver:action.
v middleButtonMenu:menu.
v open.
#middleButtonMenu:
message.
View
may redefine the #menuHolder
message, and return something else).
The menuHolder (i.e. either the model or the view itself) is asked for
the menu by being sent a menuMessage.
This selector can be defined for any view via
'menuMessage:aSelector
'.
All views which can operate both with and without a model
preset the menuMessage to something they understand and
respond with an appropriate menu
- you do not have to care for this if no model is involved.
However, if you set a views model, it must respond to the menuMessage
and return either nil or an appropriate popupMenu.
Lets put this theory into a more concrete example;
of course, the model should respond to
|model view|
model := Plug new.
model respondTo:#modelsMenu with:[PopUpMenu labels:#('foo' 'bar')
selectors:#(foo bar)].
view := View on:model.
view menuMessage:#modelsMenu.
view open
#foo
and #bar
;
to make things more interesting, lets make the models menu response dependent
on some internal models state:
The above was a somewhat simple example,
in that the view has no builtin default menu.
|model state view|
state := #fooState.
model := Plug new.
model respondTo:#modelsMenu with:[
state == #fooState ifTrue:[
PopUpMenu
labels:#('foo' 'bar')
selectors:#(foo bar)
] ifFalse:[
PopUpMenu
labels:#('jabbadabbadooo')
selectors:#(jabberwoky)
]].
model respondTo:#jabberwoky with:[state := #fooState].
model respondTo:#foo with:[state := nil].
model respondTo:#bar with:[state := nil].
view := View on:model.
view menuMessage:#modelsMenu.
view open
For views which offer their own menu (all text views do),
there is a slight complication introduced by the fact that
you may want to have the view operate on the model,
but do not want to redefine its menu.
(i.e. you would like to keep the views default menu)
To allow for this setup (i.e. having a non-nil model and
use the views menu), and provide a useful default setup,
all view classes which operate on text and have a builtin menu,
allow for redefinition of the menuHolder and menuPerformer.
Setting an explicit menuHolder defines the instance which shall provide the menu
(it defaults to the model). Setting the menuPerformer defines the instance which
shall get the menu messages (this defaults to the view).
First a (half working) example:
in the above, the model provides the menu, but menu messages are still sent to the view
- which does of course not respond to the #foo message.
(however, editing works)
|model modelsText view|
modelsText := 'hello world'.
model := Plug new.
model respondTo:#modelsText
with:[modelsText].
model respondTo:#modelsText:
with:[:arg |
modelsText := arg.
Transcript showCR:'new text: ' , arg
].
model respondTo:#modelsMenu
with:[PopUpMenu
labels:#('copy' 'cut' 'paste' -
'accept' '-' 'foo' 'bar')
selectors:#(copySelection cut paste nil
accept nil foo bar)
].
view := EditTextView on:model.
view aspect:#text; listMessage:#modelsText; changeMessage:#modelsText:.
view menuMessage:#modelsMenu.
view open
Setting the menuPerformer, we get:
But now, you may ask yourself, what happens with the copy-cut-paste messages
which are to be performed by the view (not the model).
|model modelsText view|
modelsText := 'hello world'.
model := Plug new.
model respondTo:#foo
with:[Transcript showCR:'the foo action'].
model respondTo:#modelsText
with:[modelsText].
model respondTo:#modelsText:
with:[:arg |
modelsText := arg.
Transcript showCR:'new text: ' , arg
].
model respondTo:#modelsMenu
with:[PopUpMenu
labels:#('copy' 'cut' 'paste' -
'accept' '-' 'foo' 'bar')
selectors:#(copySelection cut paste nil
accept nil foo bar)
].
view := EditTextView on:model.
view aspect:#text; listMessage:#modelsText; changeMessage:#modelsText:.
view menuMessage:#modelsMenu.
view menuPerformer:model.
view open
This is already handled, with a little trick: if the menuPerformer does not respond to
a menu message, it is retried for the view.
Thus in the above example, copy-cut-paste are performed by the
view while the other messages are sent to the model.
You may even have some of the views messages be sent in the model
(because it comes first in the try-sequence):
|model modelsText view|
modelsText := 'hello world'.
model := Plug new.
model respondTo:#foo
with:[Transcript showCR:'the foo action'].
model respondTo:#cut
with:[Transcript showCR:'no menu cut allowed'].
model respondTo:#modelsText
with:[modelsText].
model respondTo:#modelsText:
with:[:arg |
modelsText := arg.
Transcript showCR:'new text: ' , arg
].
model respondTo:#modelsMenu
with:[PopUpMenu
labels:#('copy' 'cut' 'paste' -
'accept' '-' 'foo' 'bar')
selectors:#(copySelection cut paste nil
accept nil foo bar)
].
view := EditTextView on:model.
view aspect:#text;
listMessage:#modelsText;
changeMessage:#modelsText:.
view menuMessage:#modelsMenu.
view menuPerformer:model.
view open
|topView menu|
topView := StandardSystemView new.
menu := PullDownMenu in:topView.
topView open
it sets its origin and extend to some useful default (full width);
|topView menu|
topView := StandardSystemView new.
menu := PullDownMenu in:topView.
menu origin:0.0 @ 0.0 corner:(0.5 @ menu height).
topView open
notice, that in the above example, the menus current height is used - this is
its default height, which it computed from the fonts height.
its items are defined with the #labels:
message, passing
a collection of string labels:
to access the items further, each one should be associated with some
key; this is done with the
|topView menu|
topView := StandardSystemView new.
topView extent:400@400.
menu := PullDownMenu in:topView.
menu origin:0.0 @ 0.0 corner:(0.5 @ menu height).
menu labels:#('about' 'file' 'help').
topView open
selectors:
message:
finally, submenus can be defined for each item as in:
|topView menu|
topView := StandardSystemView new.
topView extent:400@400.
menu := PullDownMenu in:topView.
menu origin:0.0 @ 0.0 corner:(0.5 @ menu height).
menu labels:#('about' 'file' 'help').
menu selectors:#(#about #file #help).
topView open
the individual submenus are instances of
|topView menu|
topView := StandardSystemView new.
topView extent:400@400.
menu := PullDownMenu in:topView.
menu origin:0.0 @ 0.0 corner:(0.5 @ menu height).
menu labels:#('about' 'file' 'help').
menu selectors:#(#about #file #help).
menu at:#about
putLabels:#('about menus ...')
selectors:#(#about)
receiver:nil. "/ typically some applicationModel here
menu at:#file
putLabels:#('new' 'open ...' nil 'quit')
selectors:#(#new #open nil #quit)
receiver:nil. "/ typically some applicationModel here
topView open
MenuView
; therefore,
these support various different mechanisms. For example, instead of sending
messages to some receiver, a model can be notified or blocks be evaluated:
|topView menu|
topView := StandardSystemView new.
topView extent:400@400.
menu := PullDownMenu in:topView.
menu origin:0.0 @ 0.0 corner:(0.5 @ menu height).
menu labels:#('about' 'file' 'help').
menu selectors:#(#aboutMenu #fileMenu #help).
menu at:#aboutMenu
putLabels:#('about menus ...')
selectors:#(#about)
receiver:nil. "/ typically some applicationModel here
(menu subMenuAt:#aboutMenu)
actionAt:#about put:[AboutBox open].
menu at:#fileMenu
putLabels:#('new' 'open ...' nil 'quit')
selectors:#(#new #open nil #quit)
receiver:nil. "/ typically some applicationModel here
(menu subMenuAt:#fileMenu)
actionAt:#new put:[Transcript showCR:'new is not yet implemented'].
(menu subMenuAt:#fileMenu)
actionAt:#open put:[Transcript showCR:'open is not yet implemented'].
(menu subMenuAt:#fileMenu)
actionAt:#quit put:[topView destroy].
menu actionAt:#help put:[Transcript showCR:'help is not yet implemented'].
topView open
The following example demonstrates this:
|v1 v2|
v1 := View new extent:200@200.
v2 := InputView in:v1.
v2 origin:10@10 corner:50@50.
v2 cursor:(Cursor thumbsUp).
v1 openAndWait.
v1 displayLineFrom:0@0 to:100@100.
To do so, create a bitmap in which 1-bits represent pixels which are to be
included as view-pixels and 0-bits stand for pixels which are not.
Then define this bitmap as the views shape and (optional) border forms.
The bits need not be connected.
Example:
For portable applications, you should not use viewShapes, since not
all X servers support this.
|v viewForm borderForm|
v := View new.
"
create two bitmaps which are first cleared to zero,
then filled with a circle.
The borderShape is somewhat (2-pixels on each side)
wider than the viewShape, which defines the inside of the view.
"
borderForm := Form width:50 height:50.
borderForm clear.
viewForm := Form width:50 height:50.
viewForm clear.
borderForm fillArcX:0 y:0
width:50
height:50
from:0
angle:360.
viewForm fillArcX:1 y:1
width:48
height:48
from:0
angle:360.
"
finally set the views border- and view-Shape
"
v borderShape:borderForm.
v viewShape:viewForm.
v open
On other than X systems, it may not be supported at all.
To make your program portable across display systems, you should ask the
Display if it supports arbitrary view shapes. This is done by:
Display hasShapes
which returns true, if arbitrary shapes are supported.
Write your application to be usable with regular (rectangular) views as well.
FileBrowser new openModal
will block this view until you are finished with the fileBrowser.
And:
|b|
b := FileSelectionBox new.
b origin:0@0.
b openModeless
allows the file box to stay around.
ObjectView
class provides a framework for structured graphics.
Normally it is used as an abstract superclass, but has built in enough functionality
to be used directly as well.
ObjectView
displays and manipulates graphical objects which should be derived
from DisplayObject
or understand a similar protocol.
ObjectView
knows how to correctly redraw such a graph, how to select and highlight
elements, to scroll and zoom-in/out.
Lets start with a very simple example; lets assume, we have to display a number
of icons, which represent computers in a local network.
Each element is represented by an instance of the HostIcon
class, which defines
how instances compute their bounding box and how to redraw themself in a graphics context.
In ST/X, most views can be operated either with or without a true model. If used without a model, they typically inform others of actions and/or changes by performing so called action blocks. This is similar to callback functions in other GUI environments.
For example, a button can be told to perform some action by setting its pressaction as in:
|b|
b := Button new.
b label:'press me'.
b action:[Transcript showCR:'here I am'].
...
To make porting of MVC-based applications easier, many views also
support the well known MVC operation, in which the interaction is not
by using action blocks, but by sending messages to and receiving changes from
a model. Although this behavior can easily be simulated using action blocks,
some more support has been built into those classes to hide all those action-block
internals for those who prefer to use the model-view setup.
It is not meant that ST/X widgets are completely protocol compatible to corresponding ST-80 classes, there is no quarantee whatsoever, that applications are portable without code changes.
It should be clear, that action blocks are very flexible and allow other behavior to be easily simulated. For example, to have a button send a change-notification to some model, a block such as:
b action:[someModel change:#foo]
could be used.
Since most views have a model instance variable (and corresponding access
methods), the standard MVC Button is thus simulated by:
b pressAction:[b model value:true].
b releaseAction:[b model value:false].
#value:
, which is
backward compatible with ST-80 applications,
and also allows for a simple valueHolder to be used as model
(which will hold the buttons truth-value then).
|b m|
m := MyModelClass new.
...
b := Button new.
b label:'press me'.
b model:m.
...
this default setup arranges,
that the model gets a #value:
message
sent whenever the button is pressed.
|b m|
m := MyModelClass new.
...
b := Button new.
b label:'press me'.
b model:m.
b change:#buttonPressed.
...
this arranges, that the model gets a #buttonPressed
message
sent whenever the button is pressed.
|b m|
m := MyModelClass new.
" -> m is supposed to respond to foo:
-> with a boolean argument
"
b := Button new.
b label:'press me'.
b model:m.
b change:#foo:.
...
Finally, passing a selector for a 2-argument message will arrange for the
button to pass itself as second argument in the change message.
|v b1 b2 m|
m := MyModelClass new.
v := HorizontalPanelView new.
b1 := Button in:v.
b1 label:'press me'.
b1 model:m.
b1 change:#buttonPressed:from:.
b2 := Button in:v.
b2 label:'or me'.
b2 model:m.
b2 change:#buttonPressed:from:.
v open
the models #buttonPressed:from:
method will get the button
as the from:
argument.
The following concrete example creates a Toggle operating
on a valueHolder. ValueHolders are very simple models which do not provide much
more functionality than holding some value (in this case: the toggles state)
and informing others of changes. The valueHolders value is accessed via
#value
and #value:
messages.
the last statement opens an inspector on the valueHolder;
see the field named value and how it changes when the toggle
changes state.
|t m|
m := ValueHolder with:false.
t := Toggle on:m.
t label:'press me'.
t open.
m inspect
Notice the similarity with the actionBlock setup; telling the
toggle to NOT acquire the boolean state (via the aspect-message),
and replacing the model by a block, gives the callback setup:
Notice, that in the previous example, the model must be set AFTER we change
the toggles aspecMessage - the reason is that the toggle tries to acquire the
model-value when the model is assigned. If this is done first, the default
aspect selector (#value) would be used, which is not the correct message for
the block.
|t m|
m := [:val | Transcript showCR:('toggled to: ' , val printString)].
t := Toggle new.
t label:'press me'.
t aspectMessage:nil.
t model:m.
t open.
Example: creating two toggles on different valueHolders
Models inform their dependents about value changes;
valueHolders send a "
|v b1 b2 m1 m2|
m1 := ValueHolder newBoolean. "/ this is a short for ... with:false.
m2 := ValueHolder newBoolean.
v := HorizontalPanelView new.
b1 := Toggle in:v.
b1 label:'press me'.
b1 model:m1.
b2 := Toggle in:v.
b2 label:'or me'.
b2 model:m2.
v extent:(v preferredExtent); open.
m1 inspect. "/ have a look at the value instance variable
m2 inspect. "/ have a look at the value instance variable
self changed:#value
", which is the default
aspect of most views (and therefore, the changeSymbol on which they update
their view).
Toggles (like all others) monitor their models value, and update
themself when receiving a change notification.
If multiple views operate on the same model, all of them
will correctly show the current value.
The following opens two independent toggles on the same valueHolder model:
|v b1 b2 m|
m := ValueHolder with:true.
b1 := Toggle on:m.
b1 label:'press me'.
b2 := Toggle on:m.
b2 label:'or me'.
b1 open.
b2 open.
m inspect. "/ have a look at the value instance variable
|v f m|
m := ValueHolder with:'hello'.
f := EditField on:m.
f open.
m inspect. "/ have a look at the value instance variable
however, the editField operates on a copy of the original string,
until its contents is accepted.
Only when accepted, will the editFIeld send #value:
(or any other changeMessage) to its model to change the actual value,
passing the changed string as argument.
Return
key is pressed in the editField
#acceptOnReturn:false
)
#acceptOnLeave:true
)
CursorUp
, CursorDown
,
PreviousField
, PreviousField
and Tab
by default; The editFields leaveKeys can be changed.
alwaysAccept:true
.
|box field model|
model := ValueHolder with:'hello'.
box := DialogBox new.
box addTextLabel:'Enter some string, please:'.
box addInputFieldOn:model.
box addAbortButton; addOkButton.
box open.
box accepted ifTrue:[
Transcript showCR:'exit with ok'
] ifFalse:[
Transcript showCR:'exit with abort'
].
Transcript showCR:'Model is: ' , model value
The DialogBox
, EditField
and EnterFieldGroup
classes include more examples in their documentation protocol.
|t myModel|
myModel := Plug new.
myModel respondTo:#getText with:['hello world'].
t := TextView new on:myModel aspect:#aspect; listMessage:#getText.
t open.
whenever the model sends a change of #aspect
, the textview will
acquire its new contents by sending #gettext
to the model.
|t myModel|
myModel := Plug new.
myModel respondTo:#getText with:['hello world'].
myModel respondTo:#accept: with:[:newString | Transcript showCR:newString].
t := (EditTextView on:myModel)
aspect:#aspect;
listMessage:#getText;
change:#accept:.
t open
the textView will acquire the text to be shown via the #aspect
message,
whenever the aspect changes.
An accept will lead to the #accept:
message being sent to the model,
with the new text as argument.
|l myModel|
myModel := MyModelClass new.
l := SelectionInListView
on:myModel
printItems:true
oneItem:true
aspect:#aspect
change:#selectionChanged:
list:#getList
menu:#getMenu
initialSelection:#initialSelection
useIndex:true
l open
In the above, the selectionInListView will ask the model for the list to be
displayed whenever the aspect defined by #aspect
changes.
This change is signalled by the model in doing a self changed:#aspect
.
Since the selectionInListView installs itself as dependent of the model,
it will get an update notification whenever that happens.
#getList
to the model. The model should return an appropriate
collection entries. If printItems was set to true (as in the above case),
these entries are not taken directly, but instead, #printString is applied to
each to get the strings which are actually displayed.
#getList
message
- except, if useIndex was true (as in the above);
in this case, the numeric index in the list is passed instead.
#initialSelection
should return an index or nil
- which defines if and which initial entry should be highlighted.
The discussion of the #menu
message will follow.
Most applications do not require the above generic model, but are
perfectly happy with a simpler setup, where the selections list is
kept together with its selection in an instance of SelectionInList
.
This selectionInList keeps track of the item list and the selection, and
tell its dependents about changes. If multiple views are hooked to the
same, all of them will update as appropriate.
Example:
|l myList|
myList := SelectionInList new.
myList list:#('one' 'two' 'three' 'four').
l := SelectionInListView on:myList.
l open
Multiple views operating on the same list:
|l1 l2 myList|
myList := SelectionInList new.
myList list:#('one' 'two' 'three' 'four').
l1 := SelectionInListView on:myList.
l1 open.
l2 := SelectionInListView on:myList.
l2 open.
this also works if the presentation is different, as in:
|l1 l2 myList|
myList := SelectionInList new.
myList list:#('one' 'two' 'three' 'four').
l1 := SelectionInListView on:myList.
l1 open.
l2 := PopUpList on:myList.
l2 open.
in you application, to get informed about selection changes, try:
|l myList myApp|
myApp := Plug new.
myApp respondTo:#theSelectionHasChanged
with:[Transcript showCR:'wow, it has changed'].
myList := SelectionInList new.
myList list:#('one' 'two' 'three' 'four').
myList onChangeSend:#theSelectionHasChanged to:myApp.
l := SelectionInListView on:myList.
l open
|person firstName lastName dateOfBirth
box field|
person := Plug new.
firstName := 'John'.
lastName := 'Sampleman'.
dateOfBirth := Date day:1 month:6 year:1955.
person respondTo:#firstName with:[firstName].
person respondTo:#firstName: with:[:arg | firstName := arg].
person respondTo:#lastName with:[lastName].
person respondTo:#lastName: with:[:arg | lastName := arg].
person respondTo:#dateOfBirth with:[dateOfBirth].
person respondTo:#dateOfBirth: with:[:arg | dateOfBirth := arg].
box := DialogBox new.
box addTextLabel:'Person data:'.
field := box addInputFieldOn:(ProtocolAdaptor subject:person
accessPath:#(firstName)).
box addVerticalSpace.
field := box addInputFieldOn:(ProtocolAdaptor subject:person
accessPath:#(lastName)).
box addVerticalSpace.
field := box addInputFieldOn:(ProtocolAdaptor subject:person
accessPath:#(dateOfBirth)).
field converter:(PrintConverter new initForDate).
box addVerticalSpace.
box addAbortButton; addOkButton.
box open.
Transcript showCR:'Name: ' , firstName , ' ' , lastName.
Transcript showCR:' DOB: ' , dateOfBirth printString.
Using different aspect and change messages:
|person firstName lastName dateOfBirth
box field|
person := Plug new.
firstName := 'John'.
lastName := 'Sampleman'.
dateOfBirth := Date day:1 month:6 year:1955.
person respondTo:#firstName with:[firstName].
person respondTo:#firstName: with:[:arg | firstName := arg].
person respondTo:#lastName with:[lastName].
person respondTo:#lastName: with:[:arg | lastName := arg].
person respondTo:#dateOfBirth with:[dateOfBirth].
person respondTo:#dateOfBirth: with:[:arg | dateOfBirth := arg].
box := DialogBox new.
box addTextLabel:'Person data:'.
field := box addInputFieldOn:person.
field aspect:#firstName; change:#firstName:.
box addVerticalSpace.
field := box addInputFieldOn:person.
field aspect:#lastName; change:#lastName:.
box addVerticalSpace.
field := box addInputFieldOn:person.
field aspect:#dateOfBirth; change:#dateOfBirth:.
field converter:(PrintConverter new initForDate).
box addVerticalSpace.
box addAbortButton; addOkButton.
box open.
Transcript showCR:'Name: ' , firstName , ' ' , lastName.
Transcript showCR:' DOB: ' , dateOfBirth printString.
Please expect more information and examples to be added here ....
|app|
app := WorkspaceApplication new.
app open.
Notice, that when you evaluate this in a workspace, your workspace regains control after the
new window has opened.
Any application can be opened as a dialog using the openModal
message.
For example, the same workspace application from the previous example is opened as
a modal window with:
Notice, that the caller is blocked until the window is closed.
|app|
app := WorkspaceApplication new.
app openModal.
openAs:
with a symbolic parameter for even more combinations.
The following gives a modal window without any decoration:
|app|
app := WorkspaceApplication new.
app openAs:#popUp.
To get a modeless window without decoration, use:
|app|
app := WorkspaceApplication new.
app openAs:#popUpNotModal.
Finally, there is a modal window with a smaller decoration:
|app|
app := WorkspaceApplication new.
app openAs:#toolDialog.
As described in a previous chapter, each topView and all of its subviews are put into a common windowGroup and get their events via a single event queue in their common windowSensor. Also, a single thread of control (process) handles all events for all windows within one windowgroup.
The normal situation is to have one single windowGroup per topView, however, in ST/X, it is also possible to put multiple topViews into the same windowGroup. All views within one windowGroup will then be served by a single (lightWeight) process within smalltalk. Thus, applications which require synchronization among actions performed in multiple views can use this without a need for semaphores and critical regions.
Also, views within a windowGroup can be setup to control each other with respect
to iconification, deiconification and window closing.
To setup a master-slave relationShip between two views
(in which the slave gets destroyed/iconified automatically,
whenever the master is destroyed/iconified), use a setup as:
In the above, the slaveView is automatically closed when the master is closed.
However, the master is not affected by the slave being closed.
|masterView slaveView|
masterView := StandardSystemView new.
masterView label:'master'.
masterView extent:300@300.
masterView beMaster.
masterView open.
masterView waitUntilVisible.
slaveView := StandardSystemView new.
slaveView label:'slave'.
slaveView extent:300@300.
slaveView beSlave.
slaveView openInGroup:(masterView windowGroup).
For a mutual partnership, use #bePartner
, as in:
Since partners all collapse into one common icon, you should give
all of them the same icon bitmap and icon label (to avoid confusing the user):
|partnerView1 partnerView2|
partnerView1 := StandardSystemView new.
partnerView1 label:'partner1'.
partnerView1 extent:300@300.
partnerView1 bePartner.
partnerView1 open.
partnerView1 waitUntilVisible.
partnerView2 := StandardSystemView new.
partnerView2 label:'partner2'.
partnerView2 extent:300@300.
partnerView2 bePartner.
partnerView2 openInGroup:(partnerView1 windowGroup).
You can of course have multiple master-slave or partner relations.
In general, a slave view will be closed, whenever any master or partner
is closed. A partner is closed, whenever another partner is closed.
|partnerView1 partnerView2|
partnerView1 := StandardSystemView new.
partnerView1 label:'partner1'.
partnerView1 iconLabel:'myApp'.
partnerView1 extent:300@300.
partnerView1 bePartner.
partnerView1 open.
partnerView1 waitUntilVisible.
partnerView2 := StandardSystemView new.
partnerView2 label:'partner2'.
partnerView2 iconLabel:'myApp'.
partnerView2 extent:300@300.
partnerView2 bePartner.
partnerView2 openInGroup:(partnerView1 windowGroup).
In the next example, the slave is closed whenever any of its masters
is closed - but those do not affect each other:
The following closes the slave with any of the two masters,
which are partners (i.e. control each other):
|masterView1 masterView2 slaveView|
masterView1 := StandardSystemView new.
masterView1 label:'master1'.
masterView1 extent:300@300.
masterView1 beMaster.
masterView1 open.
masterView1 waitUntilVisible.
masterView2 := StandardSystemView new.
masterView2 label:'master2'.
masterView2 extent:300@300.
masterView2 beMaster.
masterView2 openInGroup:(masterView1 windowGroup).
masterView2 waitUntilVisible.
slaveView := StandardSystemView new.
slaveView label:'slave of both'.
slaveView extent:300@300.
slaveView beSlave.
slaveView openInGroup:(masterView1 windowGroup).
Since modalBoxes can also be opened modeless,
controlPanels for applications can be created with this mechanism:
|partnerView1 partnerView2 slaveView|
partnerView1 := StandardSystemView new.
partnerView1 label:'partner1'.
partnerView1 extent:300@300.
partnerView1 bePartner.
partnerView1 open.
partnerView1 waitUntilVisible.
partnerView2 := StandardSystemView new.
partnerView2 label:'partner2'.
partnerView2 extent:300@300.
partnerView2 bePartner.
partnerView2 openInGroup:(partnerView1 windowGroup).
slaveView := StandardSystemView new.
slaveView label:'slave of both'.
slaveView extent:300@300.
slaveView beSlave.
slaveView openInGroup:(partnerView1 windowGroup).
Notice:
|panel slaveView1 slaveView2 thisGroup|
panel := Dialog new.
panel addComponent:(Button new
label:'showSlave1';
action:[slaveView1 beVisible; raise]).
panel addComponent:(Button new
label:'hideSlave1';
action:[slaveView1 beInvisible]).
panel addComponent:(Button new
label:'showSlave2';
action:[slaveView2 beVisible; raise]).
panel addComponent:(Button new
label:'hideSlave2';
action:[slaveView2 beInvisible]).
panel beMaster.
panel label:'master'.
panel openModeless.
panel waitUntilVisible.
thisGroup := panel windowGroup.
slaveView1 := StandardSystemView new.
slaveView1 label:'slave1'.
slaveView1 extent:300@300.
slaveView1 beSlave.
slaveView1 openInGroup:thisGroup.
slaveView2 := StandardSystemView new.
slaveView2 label:'slave2'.
slaveView2 extent:300@300.
slaveView2 beSlave.
slaveView2 openInGroup:thisGroup.
In the above examples, we use #waitUntilVisible
as a workaround for a somewhat strange effect: whenever a topView is mapped
(i.e. made visible), it will look for partners or slaves to map as well.
Since #open
returns immediately (i.e. creates a new
asynchronous process for the view), the slave or partner view gets created
before (but not mapped) the first view becomes mapped.
Therefore it would immediately map the slave/partner and not give you
a chance to position the view with the window managers ghostline mechanism.
If you remove the #waitUntilVisible
messages, the slave/partner
views come up immediately. However, in some applications, this effect may
be desired (so we do not call it a bug here ;-)
I.e. to bring the slave view up immediately at some fix position,
try:
the same using partners:
|masterView slaveView|
slaveView := StandardSystemView new.
slaveView label:'slave'.
slaveView origin:(Screen current extent - (300@300)).
slaveView extent:300@300.
slaveView beSlave.
slaveView beInvisible.
slaveView open.
masterView := StandardSystemView new.
masterView label:'master'.
masterView extent:300@300.
masterView beMaster.
masterView openInGroup:(slaveView windowGroup).
|partnerView1 partnerView2 partnerView3|
partnerView1 := StandardSystemView new.
partnerView1 label:'partner1'.
partnerView1 origin:(Screen current extent - (300@300)).
partnerView1 extent:300@300.
partnerView1 bePartner.
partnerView1 beInvisible.
partnerView1 open.
partnerView2 := StandardSystemView new.
partnerView2 label:'partner2'.
partnerView2 origin:0@0.
partnerView2 extent:300@300.
partnerView2 bePartner.
partnerView2 beInvisible.
partnerView2 openInGroup:(partnerView1 windowGroup).
partnerView3 := StandardSystemView new.
partnerView3 label:'partner3'.
partnerView3 extent:300@300.
partnerView3 bePartner.
partnerView3 openInGroup:(partnerView1 windowGroup).
WindowSensor
.
For example, the following lets the sensor forget all typeAhead key presses
(for all views of a group):
...
aView windowGroup sensor flushKeyboard
...
For other methods which do selective or overall flushing, see the WindowSensor's protocol
in the browser or read the event section below.
Please expect more information and examples to be added here ....
Before going into the low-level details, have a look at the geometric objects
and corresponding wrappers, which provide an intermediate level interface
for drawing.
For many mathematical geometric objects, there exist geometric classes
and corresponding wrappers, which know
how to draw them.
Consider using those in your applications, instead of drawing things on the low
level.
For example, instead of drawing a polygon manually:
aGC displayPolygon:aCollectionOfPoints.
you can also (should ?) use an instance of Polygon
,
and a drawing wrapper:
myPolygon := Polygon vertices:aCollectionOfPoints.
myPolygon displayOn:aGC.
These objects have the added advantage, that they can be used as components
of a view (much like subviews), and therefore are redrawn automatically by
the event mechanism:
...
myView := View new ....
...
p := Polygon vertices:aCollectionOfPoints.
myView addComponent:p.
...
myView addComponent:(p translatedBy:50@50) asFiller.
...
We recommend using those - they are easy to use and handle all the redrawing
stuff for you.
These send a draw command without remembering what has been drawn. Thus, unless the hardware buffers the image, the contents is lost if the view is redrawn or resized, and the corresponding area will be filled with the view's background color.
The low level draw commands are typically used in a widget's redraw method, and the widget itself remembers the contents which is to be drawn.
We will take a look at higher level drawing later.
Even if you use geometric objects and wrappers, it is useful to know the details of the underlying low level operations. The following paragraphs describe various details, some of which also affects the higher level interfaces (lineWidth, lineStyle, paint colors etc.)
All drawing in graphic contexts is done by sending it a
displayXXX
or fillXXX
message
(usually to self
or self gc
from a subclasses method).
For example, there are methods to display lines (displayLine:
),
rectangles (displayRectangle:
), strings (displayString:
)
and so on.
All of the drawing protocol is inherited from the GraphicsContext
class, which defines all drawing in an abstract way, and which is typically
redefined in a tuned or device specific version in one of its subclasses (eg. X11GraphicsContext
).
Beside views, there are
other classes which are graphicsContexts; for example, Form
(which
represents bitmap images on the display), PSGraphicsContext
(which represents
a printers page).
For most drawing operations, a single paint color is needed, which is defined by:
aGC paint:someColor
where someColor is an instance of a color.
aGC paint:(Color red).
aGC displayLineFrom:(10@10) to:(50@50).
will draw a red line.
For all examples that follow, we will (re)use the same view.
To allow access to this view in the future, we have to create it first
and define a new workspace variable which will keep a reference to it.
Execute:
then draw into it with:
|drawView top panel frame|
top := StandardSystemView extent:350@350.
top label:'DemoView'.
panel := HorizontalPanelView origin:0.0@0.0 corner:(1.0@36) in:top.
panel inset:2; horizontalLayout:#left.
frame := View origin:0.0@0.0 corner:1.0@1.0 in:top.
frame inset:5; topInset:50; level:-1.
drawView := View origin:0.0@0.0 corner:1.0@1.0 in:frame.
Button label:'clear'
action:[drawView clear]
in:panel.
Button label:'black bg'
action:[drawView viewBackground:Color black; clear]
in:panel.
Button label:'white bg'
action:[drawView viewBackground:Color white; clear]
in:panel.
Button label:'grey bg'
action:[drawView viewBackground:Color grey; clear]
in:panel.
top open.
Workspace workspaceVariableAt:#DemoView put:drawView.
to clear the view, use:
DemoView paint:(Color black).
DemoView displayLineFrom:(0@0) to:(50@50).
Once you are finished with these examples, close the view
and remove the
workspace variable with:
DemoView clear
Workspace removeWorkspaceVariable:#DemoView
Lets start with the views background.
This is not the drawing background, but
instead the default color with which the view is filled when exposed.
This filling is done automatically by the window system.
To change the views background execute:
the views appearance will not change immediately.
However, this color is used to fill exposed regions.
Try iconifying and deiconifying (or covering/uncovering) the view to
see this.
DemoView viewBackground:(Color yellow).
If you want to make the new viewBackground immediately visible,
you have to use:
You can use either a color or an image as viewBackground:
DemoView viewBackground:(Color blue).
DemoView clear.
DemoView viewBackground:(Image fromFile:'goodies/bitmaps/gifImages/garfield.gif').
DemoView clear.
By default, all coordinates are in pixels, starting with 0/0 in the upper left, advancing to the lower-right. This can be changed using a transformation. See the section below for more about this.
All operations described below use a paint when drawing;
this paint may be a color or a bitmap image, which is used for
pattern drawing. This paint color is kept in every graphics context
(and therefore also in views) as an instance variable.
You have to set it before doing the drawing operation,
and it will be used for all successive drawing unless changed.
The paint value is set with:
...
someDrawable paint:aPaint.
...
in our concrete example, you would write:
DemoView paint:Color yellow.
or:
DemoView paint:(Image fromFile:'goodies/bitmaps/gifImages/garfield.gif').
A few operations require two colors to be set for drawing;
these are the so called opaque drawing operations.
For example, when displaying a dashed line or an opaque
bitmap image, separate paint values have to be specified for
on/off dashes or on/off pixels respectively.
The two paint values are set with:
...
someDrawable paint:fgPaint on:bgPaint.
...
or, in our concrete example, you would write:
DemoView paint:(Color yellow) on:(Color red).
now, having enough background information,
lets draw some geometric shapes:
Sometimes, it is useful to specify pixel values instead of logical colors;
for example, if some area has to be inverted on the screen, you would like
to draw with a ``color'' where all pixels are turned on with a drawing
function which exclusive-or's the pixels (and not caring which color is
actually represented by these pixels).
Those colors are created by special instance creation messages to the
Color
class: #noColor
, #allColor
and #colorId:
. For more details, read the section on colors below.
or (if you have x/y coordinates available,
and want to avoid the creation of temporary points):
DemoView clear.
DemoView paint:(Color white).
DemoView displayLineFrom:(0@0) to:(50@50).
also, vector representation (polar coordinates) is sometimes handy:
DemoView clear.
DemoView paint:(Color white).
DemoView displayLineFromX:50 y:0 toX:0 y:50.
(but see also polygons below)
|p1 p2 p3 p4|
DemoView clear.
DemoView paint:(Color white).
p1 := 50 @ 50.
p2 := p1 + (Point r:50 angle:60).
p3 := p2 + (Point r:50 angle:120).
p4 := p3 + (Point r:50 angle:60).
DemoView displayLineFrom:p1 to:p2.
DemoView displayLineFrom:p2 to:p3.
DemoView displayLineFrom:p3 to:p4.
DemoView clear.
DemoView paint:(Color red).
DemoView displayRectangle:(1@1 corner:50@50).
there are also methods which expect the rectangles values as separate arguments:
DemoView clear.
DemoView paint:(Color red).
DemoView displayRectangleOrigin:20@20 corner:50@50
or:
DemoView clear.
DemoView paint:(Color red).
DemoView displayRectangleX:10 y:10 width:20 height:20
Arcs can be drawn -
by specifying a bounding box:
or a center-point and radius:
DemoView clear.
DemoView paint:(Color green).
DemoView displayArcX:0 y:0
width:50 height:50
from:0 angle:180
if the bounding box is not square, you get (part of) an ellipse:
DemoView clear.
DemoView paint:(Color white).
DemoView displayArc:(25@25)
radius:25
from:180 angle:180
of course, 360 degrees make a full ellipse or circle; as in:
DemoView clear.
DemoView paint:(Color green).
DemoView displayArcX:0 y:0
width:75 height:25
from:0 angle:180
or:
DemoView clear.
DemoView paint:(Color green).
DemoView displayArcX:0 y:0
width:75 height:25
from:0 angle:360
for full circles, there is a shorter method available:
DemoView clear.
DemoView paint:(Color green).
DemoView displayArcX:0 y:0
width:50 height:50
from:0 angle:360
or:
DemoView clear.
DemoView paint:(Color red).
DemoView displayCircleX:25 y:25 radius:25
DemoView clear.
DemoView paint:(Color red).
DemoView displayCircle:(50@50) radius:25
|p|
p := Array with:(10@10)
with:(75@20)
with:(20@75)
with:(10@10).
DemoView clear.
DemoView paint:(Color magenta).
DemoView displayPolygon:p
DemoView clear.
DemoView paint:(Color cyan).
DemoView font:(Font family:'courier'
face:'medium'
style:'roman'
size:12).
DemoView displayString:'hello' x:20 y:50
notice, that the y coordinate defines the position where the baseline of the characters
is drawn. You may have to ask the font for the ascent (the number
of pixels above the baseline), its descent (the number of pixels below)
or its height (the sum of both).
aView font
.
|h ascent font|
DemoView clear.
DemoView paint:(Color white).
font := Font family:'courier'
face:'medium'
style:'roman'
size:12.
font := font onDevice:(DemoView device).
DemoView font:font.
h := font height.
ascent := font ascent.
DemoView displayString:'hello' x:20 y:ascent.
DemoView displayString:'there' x:20 y:(ascent + h)
For now, ignore the "font onDevice:"
stuff - this will be explained below.
Smalltalk/X supports drawing of strings along an arbitrary
line; you may pass an angle (in degrees, clockwise from horizontal),
which is treated as a baseline on which characters are drawn:
of course, the angle is not limited to multiples of 90:
DemoView clear.
DemoView paint:(Color red).
DemoView font:(Font family:'courier'
face:'medium'
style:'roman'
size:12).
DemoView displayString:'hello' x:20 y:10 angle:90
for a demonstration, resize the demoView:
then draw strings with:
DemoView topView extent:400@480.
(the lines are drawn to show the baselines of the strings.)
|p1 p2|
DemoView clear.
DemoView paint:(Color red).
0 to:359 by:22.5 do:[:angle |
p1 := 200@200.
p2 := p1 + (Point r:200 angle:angle).
DemoView displayLineFrom:p1 to:p2.
].
DemoView paint:(Color blue).
DemoView font:(Font family:'courier'
face:'medium'
style:'roman'
size:12).
0 to:359 by:22.5 do:[:angle |
DemoView
displayString:(' ' , angle printString , ' degrees')
x:200 y:200
angle:angle
]
|i|
i := Image fromFile:'libtool/bitmaps/SBrowser.xbm'.
DemoView clear.
DemoView paint:(Color cyan).
DemoView displayForm:i x:20 y:50
there are also filling versions of the above:
Filled rectangles:
Filled arcs, ellipses & circles:
DemoView clear.
DemoView paint:(Color red).
DemoView fillRectangle:(1@1 corner:50@50).
or:
DemoView clear.
DemoView paint:(Color green).
DemoView fillArcX:0 y:0
width:50 height:50
from:0 angle:90
or:
DemoView clear.
DemoView paint:(Color white).
DemoView fillArc:(25@25)
radius:25
from:180 angle:180
or:
DemoView clear.
DemoView paint:(Color yellow).
DemoView fillArc:(25@25)
radius:25
from:180 angle:180
or:
DemoView clear.
DemoView paint:(Color green).
DemoView fillArcX:0 y:0
width:50 height:50
from:0 angle:360
or:
DemoView clear.
DemoView paint:(Color red).
DemoView fillArcX:0 y:0
width:75 height:25
from:0 angle:360
Filled polygons:
DemoView clear.
DemoView paint:(Color green).
DemoView fillCircleX:25 y:25 radius:25
Have a look at the GraphicsContext-class for even more drawing
methods.
|p|
p := Array with:(10@10)
with:(75@20)
with:(20@75).
DemoView clear.
DemoView paint:(Color magenta).
DemoView fillPolygon:p
Forms and strings can also be drawn with both foreground and
background colors. This is done by the displayOpaqueString:
and displayOpaqueForm:
methods.
These will draw 1-bits using the current paint color, and 0-bits
using the background-paint color. The background color can be either
defined
together with the paint color in the paint:on:
message,
or separate with the bgPaint:
message.
Examples:
DemoView clear.
DemoView paint:(Color red) on:(Color yellow).
DemoView font:(Font family:'courier'
face:'medium'
style:'roman'
size:12).
DemoView displayOpaqueString:'hello' x:20 y:50
|f|
f := Image fromFile:'libtool/bitmaps/SBrowser.xbm'.
DemoView clear.
DemoView paint:(Color red) on:(Color yellow).
DemoView displayOpaqueForm:f x:20 y:50
going back to the non-opaque versions, these do NOT modify the
pixels where 0-bits are in the image/string.
|bits|
DemoView clear.
DemoView paint:(Color red) on:(Color yellow).
"
draw a string using both foreground and background colors
"
DemoView font:(Font family:'courier'
face:'medium'
style:'roman'
size:12).
DemoView displayOpaqueString:'hello' x:0 y:15.
bits := Image
width:16
height:16
fromArray:#[
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111].
DemoView paint:(Color green).
"
draw 1-bits only
"
DemoView displayForm:bits x:0 y:0
Using bitmaps as paint:
Smalltalk/X not only supports colors as paint/bgPaint -
you can also specify bitmaps to draw with.
example:
or:
|pattern|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
DemoView clear.
"
draw a wide line using that 'pattern'-color
"
DemoView paint:pattern.
DemoView lineWidth:10.
DemoView displayLineFromX:10 y:10 toX:80 y:40.
(see a more detailed description of joinStyle below).
|pattern poly|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
DemoView clear.
"
draw a wide line using that 'pattern'-color
"
DemoView paint:pattern.
DemoView lineWidth:10.
DemoView joinStyle:#round.
poly := Array with:(50 @ 10)
with:(90 @ 90)
with:(10 @ 90)
with:(50 @ 10).
DemoView displayPolygon:poly.
of course, filling works too:
the same is true for strings:
|pattern|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
DemoView clear.
DemoView paint:pattern.
DemoView fillCircle:(50@50) radius:25.
opaque strings:
|pattern|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
DemoView clear.
DemoView paint:pattern.
DemoView font:(Font family:'helvetica'
face:'bold'
style:'roman'
size:24).
DemoView displayString:'Wow !' x:10 y:50
another opaque string:
|pattern|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
DemoView clear.
DemoView paint:pattern on:(Color yellow).
DemoView font:(Font family:'helvetica'
face:'bold'
style:'roman'
size:24).
DemoView displayOpaqueString:'Wow !' x:10 y:50
finally, an opaque string with both fg and bg being patterns:
|pattern|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
DemoView clear.
DemoView paint:(Color yellow) on:pattern.
DemoView font:(Font family:'helvetica'
face:'bold'
style:'roman'
size:24).
DemoView displayOpaqueString:'Wow !' x:10 y:50
and bitmaps:
|pattern1 pattern2|
pattern1 := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
pattern2 := Image fromFile:'libwidg3/bitmaps/granite.tiff'.
DemoView clear.
DemoView paint:pattern1 on:pattern2.
DemoView font:(Font family:'helvetica'
face:'bold'
style:'roman'
size:24).
DemoView displayOpaqueString:'Wow !' x:10 y:50
or:
|pattern bits|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
DemoView clear.
DemoView paint:pattern.
bits := Image fromFile:'libtool/bitmaps/SBrowser.xbm'.
bits := bits magnifiedBy:(2 @ 2).
DemoView displayForm:bits x:5 y:5
opaque bitmaps (foreground is a pattern, background a color):
|bits pattern|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
bits := Image
width:16
height:16
fromArray:#[
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r11111111 2r00000000
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111
2r00000000 2r11111111].
DemoView clear.
DemoView paint:pattern.
DemoView displayForm:bits x:0 y:0
or (foreground is a color, background a pattern):
|pattern bits|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
bits := Image fromFile:'libtool/bitmaps/SBrowser.xbm'.
bits := bits magnifiedBy:(2 @ 2).
DemoView clear.
DemoView paint:pattern on:Color yellow.
DemoView displayOpaqueForm:bits x:0 y:0
or even (both foreground and background are patterns):
|pattern bits|
pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
bits := Image fromFile:'libtool/bitmaps/SBrowser.xbm'.
bits := bits magnifiedBy:(2 @ 2).
DemoView clear.
DemoView paint:Color yellow on:pattern.
DemoView displayOpaqueForm:bits x:0 y:0
|pattern1 pattern2 bits|
pattern1 := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.
pattern2 := Image fromFile:'libwidg3/bitmaps/granite.tiff'.
bits := Image fromFile:'libtool/bitmaps/SBrowser.xbm'.
bits := bits magnifiedBy:(2 @ 2).
DemoView clear.
DemoView paint:pattern1 on:pattern2.
DemoView displayOpaqueForm:bits x:0 y:0
In the above line, rectangle, polygon and arc examples, we were drawing solid
lines. You can also draw dashed lines:
the above lineStyle only draws every second dash with the current paint
color. The doubleDash mode draws every dash, with alternating paint and
backgroundPaint colors (like opaque drawing).
DemoView clear.
DemoView lineStyle:#dashed.
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView displayLineFromX:10 y:10 toX:80 y:80.
DemoView displayLineFromX:10 y:10 toX:10 y:80.
the default (if not specified otherwise) is solid:
DemoView clear.
DemoView lineStyle:#doubleDashed.
DemoView paint:(Color red) on:(Color yellow).
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView displayLineFromX:10 y:10 toX:80 y:80.
DemoView displayLineFromX:10 y:10 toX:10 y:80.
DemoView clear.
DemoView lineStyle:#solid.
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView displayLineFromX:10 y:10 toX:80 y:80.
DemoView displayLineFromX:10 y:10 toX:10 y:80.
You can set the lineWidth with #lineWidth:
. The argument
is width of the line in pixels.
a special lineWidth of 0 (zero) means: the fastest possible thin line.
This is actually a speciality of the X11 protocol. On non X11 systems,
zero-width lines are drawn as regular one-pixel lines.
DemoView clear.
DemoView paint:(Color yellow).
DemoView lineWidth:5.
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView lineWidth:10.
DemoView displayLineFromX:20 y:20 toX:80 y:80.
DemoView lineWidth:1.
DemoView displayLineFromX:10 y:30 toX:10 y:80.
Zero width lines are the default - they are usually much faster than
one-pixel lines, since a faster algorithm is often used in the X11 server.
When drawing wide lines, you may want to control how the endpoints look and how line segments of polygons and rectangles are joined. These are called capStyle and joinStyle. For thin lines, different settings may not make any visible difference.
The joinStyle controls how the lines of a rectangle or polygon are
to be connected;
The following examples show various joinStyles:
and:
DemoView clear.
DemoView paint:(Color yellow).
DemoView joinStyle:#miter. "/ thats the default anyway
DemoView lineWidth:10.
DemoView displayRectangleX:10 y:10 width:80 height:80.
it makes more of a difference with non 90-degrees angles as in:
DemoView clear.
DemoView paint:(Color yellow).
DemoView joinStyle:#round.
DemoView lineWidth:10.
DemoView displayRectangleX:10 y:10 width:80 height:80.
compare to:
DemoView clear.
DemoView paint:(Color yellow).
DemoView joinStyle:#miter. "/ thats the default anyway
DemoView lineWidth:10.
DemoView displayPolygon:(Array with:10@10
with:80@10
with:45@80
with:10@10)
or:
DemoView clear.
DemoView paint:(Color yellow).
DemoView joinStyle:#round.
DemoView lineWidth:10.
DemoView displayPolygon:(Array with:10@10
with:80@10
with:45@80
with:10@10)
DemoView clear.
DemoView paint:(Color yellow).
DemoView joinStyle:#bevel.
DemoView lineWidth:10.
DemoView displayPolygon:(Array with:10@10
with:80@10
with:45@80
with:10@10)
The capStyle controls how the endPoints of individual lines
are to be drawn:
or:
DemoView clear.
DemoView paint:(Color yellow).
DemoView capStyle:#butt. "/ thats the default anyway
DemoView lineWidth:10.
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView displayLineFromX:10 y:30 toX:80 y:80.
a special capStyle is
DemoView clear.
DemoView paint:(Color yellow).
DemoView capStyle:#round.
DemoView lineWidth:10.
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView displayLineFromX:10 y:30 toX:80 y:80.
#notLast
which suppresses drawing of
the endPoint. This is useful if lines are drawing in xor-mode (i.e. inverting),
to avoid inverting the connecting points of a polygon twice.
(Notice: you need a pixel magnifier (xmag) to see the differences
of the next two examples.)
This inverts the endPoints twice - leaving a one pixel hole:
This inverts triangle correctly:
DemoView clear.
DemoView paint:(Color allColor).
DemoView capStyle:#butt.
DemoView lineWidth:0.
DemoView function:#xor.
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView displayLineFromX:80 y:10 toX:80 y:80.
DemoView displayLineFromX:80 y:80 toX:10 y:10.
DemoView function:#copy.
DemoView paint:(Color yellow).
DemoView clear.
DemoView paint:(Color allColor).
DemoView capStyle:#notLast.
DemoView lineWidth:0.
DemoView function:#xor.
DemoView displayLineFromX:10 y:10 toX:80 y:10.
DemoView displayLineFromX:80 y:10 toX:80 y:80.
DemoView displayLineFromX:80 y:80 toX:10 y:10.
DemoView function:#copy.
DemoView paint:(Color yellow).
Every drawable supports coordinate transformations. In all of the above examples, we have been drawing using device coordinates (actually, we should say: "we have been drawing using an identity transformation").
Each drawable contains a transformation object which must be
an instance of WindowingTransformation
and can be set up
to both scale and translate coordinates of the drawable.
If the transformation is set to nil, this is equivalent to the identity
transformation (i.e. scale of 1 and translation of 0).
In Smalltalk/X this transformation also affects clipping and event coordinates - thus, once you defined your logical coordinates, your view will receive button, keyboard and redraw events in logical coordinates too.
As a first, simple example, lets scale all drawing by a factor of 2:
everything will be transformed; even strings, bitmaps and line widths:
DemoView clear.
DemoView paint:(Color yellow).
DemoView lineWidth:1.
DemoView transformation:nil.
DemoView displayLineFromX:10 y:10 toX:40 y:10.
(Delay forSeconds:1) wait.
DemoView transformation:(WindowingTransformation
scale:2@2 translation:0@0).
DemoView displayLineFromX:10 y:10 toX:40 y:10.
(notice, that transformations also work with all of the above drawing
operations; you may want to try the opaque string and bitmap examples above
again with scaling now in effect)
|img|
img := Image fromFile:'libtool/bitmaps/SBrowser.xbm'.
DemoView clear.
DemoView paint:(Color yellow) on:(Color red).
DemoView lineWidth:1.
DemoView transformation:nil.
DemoView displayLineFromX:10 y:10 toX:40 y:10.
DemoView displayOpaqueForm:img x:20 y:20.
DemoView displayOpaqueString:'hello' x:50 y:30.
(Delay forSeconds:1) wait.
DemoView transformation:(WindowingTransformation
scale:2@2 translation:0@0).
DemoView displayLineFromX:10 y:10 toX:40 y:10.
DemoView displayOpaqueForm:img x:20 y:20.
DemoView displayOpaqueString:'hello' x:50 y:30.
there are also convenient methods in WindowingTransformation
to setup for drawing in real-world units, such as inches or millimeters.
In the following, drawing is done in centimeters:
the same in inches:
DemoView clear.
DemoView paint:(Color yellow).
DemoView lineWidth:1.
DemoView transformation:(WindowingTransformation
unit:#cm on:Display).
DemoView displayLineFromX:0 y:0.1 toX:5 y:0.1.
0 to:5 do:[:i |
DemoView displayLineFromX:i y:0 toX:i y:0.2.
]
the above examples show one problem with scaling: you may want to label the above
line (think of axes being drawn). In this labelling, the coordinates should be
transformed, while the string itself shoul be drawn unscaled.
DemoView clear.
DemoView paint:(Color yellow).
DemoView lineWidth:1.
DemoView transformation:(WindowingTransformation
unit:#inch on:Display).
DemoView displayLineFromX:0 y:0.1 toX:3 y:0.1.
0 to:3 by:0.25 do:[:i |
DemoView displayLineFromX:i y:0 toX:i y:0.2.
]
For this, you can use displayUnscaledString:x:y:
which
transformes the x/y coordinate, but draws the unscaled (device) font.
There are also unscaled versions of the opaque string methods and
the bitmap display methods.
DemoView viewBackground:(Color blue).
DemoView clear.
DemoView paint:(Color yellow).
DemoView lineWidth:1.
DemoView transformation:(WindowingTransformation
unit:#inch on:Display).
DemoView displayLineFromX:0 y:0.1 toX:3 y:0.1.
0 to:3 by:0.25 do:[:i |
DemoView displayLineFromX:i y:0 toX:i y:0.2.
].
DemoView font:(Font family:'helvetica'
face:'medium'
style:'roman'
size:12).
0 to:3 do:[:i |
DemoView displayUnscaledString:i printString x:i y:0.3.
]
Of course, instead of using these unscaled versions, you could also switch back to an identity transformation when drawing those strings. But then, you still had to apply the transformation manually to the x/y coordinates of the strings.
To get transformed values of your coordinates, transformations may be manually applied. For example:
devicePoint := (aView transformation) applyTo:logicalPoint
or vice versa (i.e. from device coordinates back to logical coordinates):
logicalPoint := (aView transformation) applyInverseTo:devicePoint
Internally, Smalltalk/X uses the transformation also for scrolling.
Therefore, all scrolling operations are transparent to your drawing calls.
Color
represent colors in a device independent
way. Internally, they store the red, green and blue components.
Since the eye only differenciates between about 100 greylevels,
the rgb values of the actually displayed colors may be rounded somewhat,
when colors are displayed on the screen.
Instances are created by:
|myColor|
myColor := Color red:50 green:100 blue:0.
The component values are in percent, ranging from 0/0/0 (for black)
to 100/100/100 (for white). Thus, the above color should be some
lime-like green-yellow.
Color offers variants of the above instance creation message, (which may be useful, if the r/g/b values were read from some file):
myColor := Color redByte:r greenByte:g blueByte:b.
takes byte-valued integral r/g/b values (i.e. 0..255) and is useful,
if colorValues are read from an image file,
myColor := Color gray:grayPercent.
creates a gray color with given brighness
percentage (i.e. all of red, green and blue get the same)
myColor := Color cyan:c magenta:m yellow:y
uses the c/m/y color space (which is used with printing)
and finally:
myColor := Color hue:h light:l saturation:s
takes h/l/s arguments, where hue is the position on the color wheel
(in degrees 0..360), light is the brightnes of the color (0..100)
and saturation gives the amount of color (0 is gray, 100 is fully saturated).
For common colors (red, green, blue, yellow, black, white and a few more), a short instance creation protocol is also avaliable:
myColor := Color red. "or green, blue, black etc."
For ST-80 compatibility, there exists a companion class
called ColorValue
. This class expects r/g/b components
in [0..1]. Therefore, you can also write:
ColorValue red:0.5 green:1.0 blue:0.0
or:
ColorValue brightness:0.5
and get the same color as from corresponding messages to Color.
After creation, colors are not associated to a specific device.
This is done by sending onDevice:aDevice
to the color instance.
The device argument can be either a Workstation device (such as Display)
or some other medium, such as a postscript printer.
Once associated to a device, a color stores the device id of the color
(which is either the colormap index or any other device handle for the color)
internally.
All of the above described drawing methods perform this device conversion
automatically. However, to avoid this operation to be repeated over and over
with every draw, it is wise to add code to do this conversion once during
early startup of your view (if you have defined your own subclass of view).
For example, your views #initialize
method could look like:
...
super initialize.
...
myPaintColor := someColor onDevice:device.
...
and use myPaintColor for drawing operations.
Be careful, once the color is reclaimed by the garbage collector, the corresponding device color will be freed as well. This may lead to hard to find redraw bugs, if you do something like:
...
aView paint:(Color yellow).
...
aView paint:(Color green).
...
and a garbage collect occurs in between. In the above, something is drawn
in yellow, for which the next free device color is allocated.
If a garbage collect occurs in between, this colormap entry may be reclaimed and
reused for the green color. Everything drawn in yellow before will now (since
the devices colormap has changed) be shown in green too !
To avoid the above, you must keep a reference to the device color somewhere,
to prevent the garbage collector from reclaiming the color cell, as long
as the view is visible.
The best strategy is to keep all used colors in some instance variable of the
view or of your model (as was done in the above #initialize
method).
i.e.:
in an instance variable:
... myColors yellow green ...
myColors := OrderedCollection new.
yellow := Color yellow onDevice:Display.
green := Color green onDevice:Display.
myColors add:yellow; add:green.
...
aView paint:yellow.
...
aView paint:green.
...
For raster operations, special colors where the pixel value is given,
are required. These are created with:
Color noColor
Color allColor
Color colorId:aNumber
variableColor := Color variableColorOn:aDevice
this special `colors' pixelValue is fixed, but its r/g/b components
can be changed at any later time with:
variableColor red:r green:g blue:b
example:
|l cell|
cell := Color variableColorOn:(Screen current).
cell isNil ifTrue:[^ self warn:'variable color not available'].
l := Label new.
l label:('hello' asText allBold).
l foregroundColor:cell.
l open.
l := EditTextView new.
l contents:('hello\world\this is\blinking'asText
emphasizeFrom:21 to:28
with:(Array
with:#bold
with:(#color->cell))
) withCRs.
l open.
[
1 to:30 do:[:i|
i odd ifTrue:[
cell red:100 green:0 blue:0
] ifFalse:[
cell red:0 green:0 blue:0
].
Display flush.
(Delay forSeconds:0.4) wait
].
1 to:30 do:[:i|
i odd ifTrue:[
cell red:0 green:100 blue:0
] ifFalse:[
cell red:0 green:0 blue:0
].
Display flush.
(Delay forSeconds:0.4) wait
].
] fork.
Be careful when using these writable color cells: they are not
available on all display types (trueColor, greyscale or black&white systems).
Display visualType
.)
Font
represent fonts in a device independent
way. Internally, they store the name of the font as family, face, style
and size.
|myFont|
myFont := Font family:'times'
face:'medium'
style:'roman'
size:12
The size parameter is not the number of device pixels,
but instead the point size (printer units) of the font. Here, one point
is 1/72'th of an inch.
You can work with fonts in this device independent manner as long as no device specific queries are to be made. All drawing operations (i.e. displayString...) will take device independent fonts and convert themself to a device representation.
Therefore, you can use the same font object for different display
media - for example, a view and a postscript printer page.
As in:
|myFont myView myPage|
myFont := Font family:'times'
face:'medium'
style:'roman'
size:12.
myView := View in: .....
myPage := PSGraphicsContext ....
myView font:myFont.
myView displayString: ....
myPage font:myFont.
myPage displayString: ....
However, as soon as you need some physical characteristics of a font,
you need a font to be bound to the specific device.
onDevice:aDevice
"
to a font (similar to the color handling).
The returned value is an instance of Font which represents the same font as
the original, but is bound to that device (if the original was already
for that device, this is a noop and the original font is returned.
Therefore, if in doubt, use this conversion;
it does not hurt).
In particular, this conversion is required when asking a font for its ascent, descent, height and some other device specific attributes.
You will get an error (debugger) when asking a font which is not bound to a device about these attributes.
As a summary, have a look at the following code fragment:
|f|
f := Font family:'times'
face:'medium'
style:'roman'
size:18.
f := f onDevice:(DemoView device).
DemoView displayString:'hello' x:0 y:(f ascent).
DemoView displayString:'world' x:0 y:(f height + f ascent).
in the above, ascent
asks for the number of pixels
above the baseline (remember: displayString's y argument specifies the
y coordinate of the baseline) and
height
asks for the fonts overall height (i.e. ascent plus
descent).
A cursor can be created from a bitmap array, from a form or from an image.
Also, for the most commonly used cursors, you will find convenient
methods in Cursors class protocol.
For example, the waiting hourglass cursor is returned by:
Cursor wait
or a standard arrow cursor is returned by:
Cursor normal
To load a cursor from a bitmap image found in a file,
first read the file into an image instance, then convert it
into a cursor.
For example:
|img cursor|
img := Image fromFile:'goodies/bitmaps/xpmBitmaps/cursors/cross2.xpm'.
cursor := Cursor fromImage:img.
Once the cursor is created, you can set it in any view with:
aView cursor:someCursor
or, a concrete example using above demoView:
DemoView cursor:(Cursor wait)
There are also convenient methods to change the cursor in all views
which belong to a common windowGroup - either permanently or
for the duration of some block evaluation.
(You can ask every view about its
windowGroup by sending it #windowGroup
).
self windowGroup withCursor:(Cursor wait)
do:[
...
... long computation
...
]
using this assures correct restoration of the original cursors
- even in case of an aborted or terminated computation.
self withCursor:(Cursor wait)
do:[
...
... long computation
...
]
Here, only that single views cursor is changed for the duration of
the block evaluation.
Finally, standardSystemViews also offer this interface to change
the cursor in itself and all of its subviews. Since all topviews
are instances of StandardSystemView
(or of its subclasses),
a busy view can also use:
self topView withCursor:(Cursor wait)
do:[
...
... long computation
...
]
So you can decide if a wait cursor is to be shown in a single view
in a single topView with all of its subviews,
or in all views belonging to that windowGroup.
For ST-80 compatibility, cursors also support the
showWhile:
message. This will display the receiver cursor
in ALL views while evaluating the block argument.
The event dispatcher will read the event and put it into an event queue.
Then the associated windowGroup process gets a signal that some work is
to be done.
The event processing method (in WindowGroup
) fetches the event
from the queue
and sends a corresponding message to the view in question.
(see also: ``views and processes''.)
All event forwarding is concentrated in one single method
(WindowEvent class sendEvent:...
). Therefore, additional
or different event processing functionality can be easily added there.
keyPress:keyCode x:x y:y
keyRelease:keyCode x:x y:y
For motion and enter/leave events, the button-state is encoded as bits in the buttonState argument, which is an integer number.
buttonPress:button x:x y:y
buttonRelease:button x:x y:y
buttonMotion:buttonState x:x y:y
pointerEnter:buttonState x:x y:y
pointerLeave:buttonState
mouseWheelMotion:buttonState x:x y:y amount:amount deltaTime:dTime
exposeX:x y:y width:w height:h:
mapped
unmapped
visibilityChanged:
configureX:x y:y width:w height:h:
SimpleView
and especially the WindowSensor
classes...
deviceXXX
methods,
which actually do the transformation and
call for one of the above methods with the logical coordinates.
deviceButtonPress:x:y:
in your view class.
See the classes WindowSensor
, WindowGroup
and
WindowEvent
for more (internal) details.
enableButtonEvents
- enables buttonPress and buttonRelease
enableMotionEvents
- enables motion events
enableEnterLeaveEvents
- enables pointerEnter and pointerLeave
enableEvent:
eventSymbol - enables a specific event
disbaleButtonEvents
- disables buttonPress and buttonRelease
disableMotionEvents
- disables motion events
disableEvent:
eventSymbol - disables a specific event
See the enableXXX
and disableXXX
methods in
the PseudoView
class for more details.
keyPress:key x:x y:y view:aView
keyRelease:key x:x y:y view:aView
buttonPress:button x:x y:y view:aView
buttonShiftPress:button x:x y:y view:aView
buttonMultiPress:button x:x y:y view:aView
buttonRelease:button x:x y:y view:aView
buttonMotion:buttonMask x:x y:y view:aView
pointerEnter:state x:x y:y view:aView
pointerLeave:state view:aView
aView delegate:theDelegate
Since the delegate may only be interested in some events (and let others
be handled as usual), it will be asked by another message before the above
forwarding takes place. If the delegate has no interest, it should return false
on those messages; otherwise true.
The corresponding query messages are:
handlesKeyPress:key inView:aView
handlesKeyRelease:key inView:aView
handlesButtonPress:button inView:aView
handlesButtonShiftPress:button inView:aView
handlesButtonMultiPress:button inView:aView
handlesButtonRelease:button inView:aView
handlesButtonMotion:buttonMask inView:aView
handlesPointerEnter:state inView:aView
handlesPointerLeave:state inView:aView
handlesXXX
methods for those that are actually to be forwarded have to be implemented.
To support both views which do event processing themselves and
views for which a controller (i.e. Smalltalk-80's
Model-View-Controller or MVC way of handling events),
events are alternatively forwarded to the controller if the views
controller instance variable is non-nil.
This makes porting of Smalltalk-80 code easier,
since all you have to do is to set a views controller
to have that controller process these events .
Delegation via the delegate takes precedence over the controller. This allows event delegation even for views which have a controller; therefore, this allows adding/modifying the behavior of existing widgets without a need to modify these and/or define a new controller class. (For example, additional keyboard shortcuts can be easily implemented using the delegation mechanism.)
aDisplayDevice
sendKeyOrButtonEvent:typeSymbol
x:xPos
y:yPos
keyOrButton:keySymCodeOrButtonNr
state:stateMask
toViewId:targetId
To send a string to some other view (as if it was typed in), there is a more convenient
interface, which breaks up the string into individual characters, and sends individual
KeyPress and KeyRelease events:
aDisplayDevice simulateKeyboardInput:aCharacterOrString inViewId:viewId
You have to acquire the alien views ID either by a query to the display:
aDisplayDevice viewIdFromPoint:point.
or by passing it somehow manually
from the other application
(deposit its ID as an XAtom, or pass it as a commandLine argument)
example:
This is a very special functionality and only recommended to help
interfacing to badly written alien applications.
|point id|
point := Display pointFromUser.
id := Display viewIdFromPoint:point.
Display simulateKeyboardInput:'Hello_world' inViewId:id
WindowSensor
class allows an object
to be registered as listener, to be sent every incoming event
before it is dispatched to the corresponding views event queue.
You can register either a global eventListener (which will get events for any
view), by registering your listener in the WindowSensor
class,
as in:
WindowSensor eventListener:someListenerObject
or, for an individual windowSensor (and therefore get the events for all of its
associated views)
aWindowSensor eventListener:someListenerObject
A concrete application of a sensor-specific listener is to display
a help or info message in some infoView (a label), whenever the pointer
enters a subview (like the lower info-field in MS-Windows applications),
or to display bubble help in some automatically popping view.
In contrast to the way delegation works, the evenetlistener is not
asked if it responds to those messages; a listener must implement and
respond to all of the above messages.
In addition, these methods must return a boolean value.
If they return true
,
the event is not dispatched to the views event queue.
If they return false
, the event is processed as usual.
As a consequence, a listener which simply returns true
in all
of its listener methods, will disable all event handling for the sensor's view
or for all views (if its a global listener).
Be careful (save your work before) when playing with this mechanism;
for the above reason, a faulty event listener may easily lead to a
locked smalltalk system
(however, you can repair things in the miniDebugger:
enter its interpreter ('I'-command) and evaluate
"WindowSensor eventListener:nil"
there).
It is not possible to listen to events of non-smalltalk (i.e. alien) views with this mechanism.
flushEventsFor:
aViewOrNil
flushUserEventsFor:
aViewOrNil
flushExposeEventsFor:
aViewOrNil
flushKeyboardFor:
aViewOrNil
Lets first repeat, how the event mechanism works in ST/X:
The physical reading of display related events is done by the event
dispatcher process (created in aDisplay startDispatch
).
This process keeps waiting for arriving events, reads them and forwards
them to a per-windowgroup event queue.
On the other side of the queue, the process associated to the windowGroup
waits on a semaphore for arriving events and handles them by forwarding
the events to the controller object which is associated to the
view which received the event (or, directly to the view, if it
happens to have no controller).
This is done in a method in WindowSensor
.
All we have to do for multiple display screens is to start an event dispatcher process for the other display, and create views on this new display.
The steps to do this are:
Smalltalk at:#Display2 put:(XWorkstation new).
this simply creates a new display object - there is no connection or
event dispatching yet. In the example above, we keep this object in a global
variable for later accessibility (in the examples below).
Display2 := Display2 initializeFor:'porty:0.0'
(this expression returns nil, if the connection is refused by the
Xserver or any other error occurs).
#initializeFor:
method is
class specific. Currently, for Xdisplays, this is the name of the
display station and its screen number (usually 0.0).
Display2 deviceIOErrorSignal handlerBlock:[:ex |
Transcript beep.
Transcript showCR:'Display (' , Display2 displayName , '): connection broken.'.
AbortSignal raise.
].
If no such handler is setup, a broken connection will lead to a
debugger to be opened on the main display (i.e. the startup display).
Display2 startDispatch
once the above has been executed, you will find the new event dispatcher process
in the ProcessMonitor ready to receive events.
(FileBrowser onDevice:Display2) open
or:
(Workspace onDevice:Display2) open
or:
(OldLauncher onDevice:Display2) open
The Smalltalk/X ApplicationModel
class has been
changed to also allow opeing its views on remote displays;
(Launcher onDevice:Display2) open
Since some of the default displays state was initialized from
your startup file (keyboard & button mappings), these are obviously
not set correctly in this new display.
Therefore, you may want to setup these with:
before opening the first views on the other display.
Additional background info:
Display2 keyboardMap:(Display keyboardMap).
Display2 buttonTranslation:(Display buttonTranslation).
Previous versions of ST/X accessed a global variable
(named "Display"
) in many places to refer to the display screen.
(actually, this was a leftover from attempts to be ST-80V2.x compatible)
For example, popUpMenus, fonts,
colors etc. were always created on this device by default.
This was changed in the newest version to use
"Screen current"
instead. This expression refers to the currently
active screen at the time the process runs, and will return different
display instances for processes running under different screeens windowGroups.
In order to avoid limiting yourself to single display operation,
do not access the global "Display"
.
If at all, the default screen should now be accessed in programs
via "Screen default"
.
(remember the section on using globals in the coding style document ?)
If you ever plan to support multiple display screens
(and, you never know for sure), you should NOT repeat
the authors bugs and also use "Screen current"
whenever creating
new views with an explicit display assignment.
If you do not give an explicit display device (i.e. you use "someView open"
),
the default device will be the one returned by above expression.
Due to the device independent way colors, cursors, fonts etc. are handled,
you do not have to change your single display application to be
operated on multiple concurrent displays.
(it looks as if the initial design of the display interface
was not too bad; after all no single line of the existing applications views
had to be changed for multiple screen support ;-)
Be reminded, that multiple screen handling has not been
fully tested and is still an experimental feature.
There may still be a few places left in the system,
which refer to the explicit "Display"
. If you encounter
any of these and have problems due to that, please let the autor know
about this. There is no warranty, that things work correctly everywhere
and popUps or dialogs will not be opened on the default screen occasionally.
ObjectMemory
to get notified of image save/restart.
It should close all remote views at image save time,
and recreate them at image restart time.
The multidisplay feature is still very usable, for special end-user applications. However, these have to care manually for access to critical shared resources (files for example).
Transcript
.
Transcript current
, which
returns a different transcript view on each remote display.
Display
global
should be taken when accessing the Transcript
in those multidisplay
applications; urgent error messages should be sent to Transcript
;
local information to Transcript current
.
#initialize
method which
does NOT do a "super initialize"
.
All redefined #initialize methods must also call for the superclasses'
initialization.
The same is true for other redefined methods: #initStyle, #mapped,
#realized and #destroy - unless you intent to trap those and
suppress certain actions.
If you do this (which is not recommended),
have a look at whats done in the superclasses method(s).
"view origin:100.0@50.0"
instead of: "view origin:100@50"
)
"view extent:1@1"
instead of:
"view extent 1.0@1.0"
)
#open
message ?
"super"
send.
#destroy
or #terminate
methods which do not call the superclasses methods via a super send.
#realize
message
(instead of #open
).
To get rid of the view, use the launcher's destroy view
function. This destroys the view physically - even if the windowGroups
process no longer handles any events.
If this fails also, the system has lost the knowledge about the view,
but failed to shut it down towards the display hardware.
If your system is still in a working condition, saving and restarting a
snapshot should help to get rid of them. If you cannot save/restart,
backup from the last saved working snapshot image, and check your changes
made since then.
#realize
to the view.
"View new realize"
vs. "View new open"
.
Wait for a while after evaluating the above expressions.
#realize
, unless
you know what you are doing.
"shown"
) - many drawing methods simply do nothing, if
this variables states that the view is fully covered or unmapped.
background info:
the #open message starts a new process, which handles the view
(and all of its subviews) events. Right after the open message, the new views
process (although created) had usually no chance to startup, initialize and
display the view. Therefore, a drawing or other access to the new view usually fails.
I.e. the following is wrong:
and may lead to some internal error, since drawing into the new view
is attemted before that view gets physically created and all of
its internal state and resources are setup correctly.
|v|
v := View new.
v open.
v displayString:'hello' at:10@10.
|v|
v := View new.
v openAndWait.
v displayString:'hello' at:10@10.
Therefore, #openAndWait is never required in methods of ApplicationModel
,
StandardSystemView
or View
, or of their subclasses
- except if views from other applications are accessed (which was
actually the case with the above doIt evaluations).
#realize
or #openModal
instead of
#open
?
#initialize
and #realize
methods return (i.e. is there an endless loop) ?
#enableXXXEvent
) ?
buttonPress:x:y:
or buttonRelease:x:y
with a bug ?
Set a breakpoint on it and see if its reached.
#deviceButtonXXX
and
not send either "super deviceButtonXXX"
or "self buttonXXX
" ?
#initialize
method which
does NOT do a "super initialize"
#widthOfContents
and #heightOfContents
in your view ?
"self changed:#sizeOfContents"
whenever appropriate (i.e. after changing the contents) ?
"self changed:#originOfContents"
whenever appropriate?
...
myFont := Font family:'courier' size:10.
...
h := myFont height.
...
you must ask a device specific font, as in:
...
myFont := Font family:'courier' size:10.
...
myFont := myFont onDevice:device.
h := myFont height.
...
(read the chapter on fonts above).
Copyright ? 1995 Claus Gittinger, all rights reserved
<cg at exept.de>