[prev] [up] [next]

Work in progress This document is being prepared - it is not complete.

Introduction to View Programming

Contents

Introduction

This document presents a way of quickly starting to use the ST/X View & Widget classes. It consists of tutorial text and code fragments to present some starting points. Of course, its far from being complete.

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.

Model-View-Controller vs. Callback Operation

Traditionally, in Smalltalk, the graphical user interface was built upon the Model-View-Controller (MVC) mechanism.
This means that the user interfacing is done by three distinct components:

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.

TopViews

In Smalltalk/X, most visible User Interface (UI) elements are derived from a common superclass, called View. A view can have subviews, and each of these subviews has that view as their common superview.
The one view in such a hierarchy, which has no superview, is called a topview. Topviews are the outermost views, which usually get decorated by labels and borders by your X-window manager.

Notice for the curious:

Topviews also have a special place in ST/X with respect to event handling:
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.)
Let us create & show our first view:
(click on the code-line below, or copy it to the workspace, and execute it with the doIt-function)

  (View new) open "create a view, and make it show itself"
The above code performs two steps:

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:


  (StandardSystemView new) open "create a view, and make it show itself"
opens a view with a (default) label, a (default) icon and a default size.

Modeless vs. Modal Open

In the above example, the newly created view was opened by sending it the open message. This creates a new process and made the view independent of the currently running process.
It can also be opened under control of the current process with:

    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.
Your application views should normally run as a separate process, except if its some kind of dialog box. If nonmodal, it will be listed as a separate UI process in the ProcessMonitor and errors occuring in it will not affect the process which created and opened your view.

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.
(i.e. sending #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.

Window labels, Icons & Titles

Lets see how a standardSystemViews can be decorated. The following defines the views window label, its icon and an icon label. (Not all windowmanagers show this icon label, though):

  |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:


    |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
Hint:
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:


    |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.
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 openIconified.
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 openAt:100@100.
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.

Window Dimensions & Position

A view will come up in its default dimension (i.e. extent), which is defined in the classes defaultExtent-method. For StandardSystemView, the default value is 640 @ 400 (read as: "640 pixels wide and 400 pixels high"),
for normal (non-topViews) views the default is 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).
In any case, you can set some limits, which the window system should honor:

    |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.
Any view can have subviews; these are either created right from the start in their superView,
as in:

    ...
    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.

Fixed Size

You can specify view position and dimension both in pixels (as above) or relative to the superviews size. If the coordinate(s) in a #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.
An example with pixel extents is:

    |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.
Using such a combination message, the above becomes:

    |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.
Especially beginners should not confuse themselves by trying to learn all of this protocol by heart in the beginning.
Instead of specifying an extent, you can also specify the position of the lower-right corner point:

    |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.
Examine the following code, and think what happens if the topView is opened-with or resized-to to an odd number 99 pixels:

    |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:


    |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
(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)

Relative Size

If a component is given as a fraction or float number, it is interpreted as relative to its superviews size. The value should be within 0.0 .. 1.0, for reasonable results
(you can specify a view to be larger than its superview - in this case, only part of the view will be visible; depending on its origin).

An example with relative extents is:


    |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
It is possible and often useful to combine a relative dimension with a pixel value.
here is an example which defines a relative width, but absolute pixel heights:

    |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 resize these topviews, the subviews position and/or dimensions will be adjusted automatically.

If you specify a relative extent for a topview, its extent is computed relative to the screen size:


    |v|
    v := StandardSystemView new.
    v origin:(0.25 @ 0.25).
    v corner:(0.75 @ 0.75).
    v open
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).

Dynamically Computed Size

The most flexible way of specifying dimensions is by passing a computation rule as a block. This block is supposed to return the new value as a point (again: either in pixels or relative).
You can do arbitrary complex size computations in these blocks. The block will be (re)evaluated whenever the superviews size changes.
Example:
(ignore the button details here - it will be described later.
Concentrate on the 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)


    |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.
the following example demonstrates the effect of the evaluation order and will not work correctly:
(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.)

    |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.
Hint:
You can force evaluation of these blocks (i.e. simulating a size-change) by sending #sizeChanged:nil to the superview or #superViewChangedSize to the view itself.
Hint:
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).
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.
Hint:
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 and VerticalPanelView; these should solve most typical arrangements without a need to setup complicated dimension blocks.

Layout Objects

In addition to the above, you can also let some other object control the dimension of a view. These objects are called layout objects, and are asked to provide a size much like the blocks described above. Actually, sizing blocks and layout object can simulate each other; they are comparable in the functionality they provide. Having both is due to historical reasons - blocks were first, and layout objects were added later for compatibility reasons, These two mechanisms may be mapped onto one internal mechanism in future versions.
Layout objects have been added to make porting of VW applications easier, since in this system, these are used to control a views dimension (you will find quite a lot of PD code, using these layout objects).

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:

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:


    |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
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:(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
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:(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
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;
			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
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.

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.

General View Appearance

A views appearance consists of many paramaters, to name some. In general, if using existing widgets for your application, you should not specify these explicit, but depend on the default values, which are provided by a so called styleSheet.
Doing so makes it possible to change the appearance of ST/X applications and make these applications fit nicely into existing frameworks.

Do not Fight the Style Sheet

The headline already says it: do not hardcode any style settings into your code. (It used to be so in previous versions of ST/X and took alot of hard work to make it more flexible - don't repeat my bugs).

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.
(all ST/X classes cache these values in class variables, and/or read them at view creation time into a views instance variable. This avoids repeated lookups for those values)

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.

Layout of Subviews

Using Simple Subviews for Layout

Typical applications consist of many elements which have to be organized into topviews. The following chapter describes how subviews are arranged and how to control the geometric layout.

For very simple layouts, use just another subview, as in:


    |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
(please read on, if you think the example does not work ...)
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 "1" instead of "1.0" as a dimension in the extent or corner parameter.

3D Level, Border & Background

You did NOT see the subviews in the above example, because all have the same background, and no border around (non-3D styles will show a border though). For some 3D effect, you can make views "come out-of" or "go-into" the display, by setting its Z-level relative to the superView.
As in:

    |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.
For the standard colors, there are also shorter messages, such as "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.

... and Background Pattern

Aaah, before I forget, not only colors can be defined as background; have a look at:

    |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.
More formats are supported (TIFF, XBM, XPM, XWD, ST80-Form, Face, Targa and Windows-bitmap). Also notice, that the above example works on any kind of display: on monochrome or greyscale displays, the image will be converted to some useful approximation (dithered).

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):


    |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
or:

    |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
As you see, constant bitmaps are defined in chunks of 8 pixels, left to right.

You can specify a colormap to be used with monochrome mitmaps too:


    |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
Just to show what is possible, try the following (buttons and insets will be explained below in detail):

    |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.
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.
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:

    |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.
The default #backgroundColor: (implemented in a common view superclass) sets the viewBackground.

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.

Inset

Lets go back to view geometry. You can also specify a so called inset on the views corners, given in pixels. If set to non-zero, the views dimension is reduced by these insets (there are four of them) after any dimension calculation (i.e. after the relative sizes are computed). Positive insets reduce the size, negative insets increase the size of the view at that corner.
This allows you to create views where the extent is based on the superviews size (i.e. relative), but offset by some fix margin.

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:


    |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
or:
(have a careful look at the labels definition - the insets have negative values ...)

    |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
HINT:
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):

    |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.
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 ...)

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.

Layout views

Labels

The simplest of the layout views is a label. Labels simply present a string or bitmap image.
example:

    |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.
In the above example, the labels real boundaries can be made visible, by setting their levels:

    |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

Positioning the Labels Contents

A labels default layout strategy is to place its logo centered in its viewing area. You can control this by setting its adjust; possible values are #left, #right, #center (which is the default) or #fit.
For example:

    |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):


    |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
here, the first two strings are centered as long as they fits the views bounds, but displayed right/left adjusted if they do not.

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:


    |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
Hint:
To avoid confusing the user, you should not use positive levels for labels, since they then look like buttons.

Panels

Often, you need to arrange many little subviews (Buttons, Labels etc.) in a view, and have them automatically rearrange, when the superview changes its size.

Horizontal and vertical panels

To do so, use one of PanelView, HorizontalPanelView and VerticalPanelView.
These layout views are preferrably used, if multiple fixed size elements have to be arranged into a row or column, and the size of the panel itself is no fixed (i.e. some rearrangement is required).

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:


    |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.
(I hope you already know some Smalltalk, to understand this ... :-)

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.

Specifying how elements are arranged
You can arrange elements different as in:

    |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.

The additional #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:


    |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.
Try the above with #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.


    |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.
The layouts supported by 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:


    |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.
(However, notice that font scaling is a slow operation on most displays; therefore consider the above example a non realistic demo)

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.
Specifying the spacing between elements
For horizontal and vertical panels, you can specify the horizontal layout to be different from the vertical layout; also, the spacing between elements can be changed in both horizontal and vertical directions.

More examples.
change the space between elements:


    |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.
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.
    panel verticalSpace: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: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.
Adding empty views allows grouping:

    |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.
combining a layout of #fit with an empty spacing, gives you dense packing:

    |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.
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|

	button := Button label:thisLabel in:panel.
	button width:1.0.
    ].
    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 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].
Can you imagine, what this does ? (try to find out before starting it :-)

You will find more examples in the classes' example category.

Layout conflicts
There are some situations, in which a panels layout strategy fails. One example is when you specify a #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).
In this setup, it is not defined which geometry will finally be taken, since it depends on the order in which resizing operations are performed.
Bad example:

    |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:


    |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.
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 #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).

Variable panels

Finally, there are panels, which allow variable relative sizes: the VariableHorizontalPanel and VariableVerticalPanel. Most browsers use these to allow for a variable ratio between their selection list and their codeview.
Try:

    |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:


    |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
to show a more complex example, the following puts constant height button panels in between the variable size views:

    |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

Invisible views

If no special precaution is made, all of a views subviews are made visible (realized) whenever that view is made visible. This default behavior relieves you from the need to walk over all subcomponents and realize them individually. However, in some situations, it may be desirable, to hide individual views; either initially or dynamically during interaction with the application.
A view can be hidden (at any time) by sending it #beInvisible and made visible again by #beVisible.
Example:

     |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).

Stacked views

It is possible to stack muliple subviews views onto each other and, depending on some external event, choose which one should be visible. The easiest way to do this is to simply define each of them as having the same origin/corner, and raise one of them to the front.

Example (this examples uses elements which will be explained later - simply concentrate on the view creation and the raise-message sent from the buttons):


    |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
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).
as in:

    |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
If you want to implement noteStack-like applications, you can use the event delegation mechanism, to catch events and raise when clicked-upon.

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

Scrolling views

Every view can be wrapped into a ScrollableView or HVScrollableView, which add vertical or vertical and horizontal scrollbars. (Currently, there is no horizontal-only wrapper - this will be added soon).
To be able to set the correct position and size of the scrollbars thumb, the view to be scrolled must respond to the following messages: Scrolling is done by changing the transformation of the scrolled view. Therefore, scrolling is almost transparent to the scrolled view. However, for performance reasons, the scrolled view may use the scroll origin and view dimensions to optimize redraw operations by limiting them to the visible area.

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.

Interactors

We call those views which allow interaction with the user interactors in contrast to layout views. In the browser, you will find these classes in the ``Views-Interactors'' class category.

Buttons

We have already met Buttons in the previous examples. Lets go into more detail here.
Since Button is a superclass of Toggle, CheckToggle and RadioButton, the following is also valid for these subclasses which are described in more detail below.

Attaching an action to a button

Whenever pressed, a button performs some action. Beside the MVC operation, in which a model is informed about the buttons state change, and which is described in more detail below, buttons also support actionBlocks. ActionBlocks are set by the application which uses a button and are evaluated whenever the button is pressed.

(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:


    |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
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).
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:

    |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
Actually, buttons really support two actions: the pressAction and the releaseAction.The #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:


    (Button label:'see the transcript') pressAction:[Transcript showCR:'pressed'; endEntry];
				      releaseAction:[Transcript showCR:'released'; endEntry];
					    open.
    Transcript topView raise
or:

    |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

Fonts and images

Hint (read before you start to change fonts in your buttons ;-):
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.
Buttons (like all other views) have a font in which they draw text. You can set the font to be used with the #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:
(you already know about panels, so the following example should be easy to understand):

    |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

Enabling & disabling

Buttons can be enabled, disabled and also be turned on and off under program control:

    |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:


    |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.
Beside the buttons interface, these examples gave us some more new information: (the reason you need the topview in the above example is, that 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.)

Different active/passive logos

Normally, only the buttons level or (for 2D styles) its colors are affected when activated. However, it may be useful to specify different logos.
Example:

    |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:
in the example above the levels and borders are explicitely turned off, since the bitmaps already include 3D effects. This may not be the case for other bitmaps.

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:
(this example expects a soundfile to be present in /usr/local/lib/sound, and a sound device to be present - this may not work on your system)

    |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.
Especially, if your button is arranged in some panel, this is definitely not what you want. Therefore, you should fix the buttons size.
(Note: it is possible to tell the panel that its components change their size and have it rearrange things whenever that happens.)

Fix size vs. variable size

You can fix a buttons size by sending 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).
Example:

    |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:
Labels (and therefore buttons, toggles and radioButtons too) offer various logo adjustment schemes, which control where the logo is to be placed within the view. The default adjustment is #center. But the above example may also look good with left adjusted logos.
You can use any of #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).
Try:

    |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

Toggles and radio buttons

A specialized button is the toggle - it will toggle its state whenever pressed. The protocol for toggles is the same as for buttons, with the exception, that a toggle defines both an onAction and an offAction.
Lets modify one of the above examples to use a toggle:

    |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.

CheckToggle and CheckBox

A special toggle is the CheckToggle, which does not display a label (-string), but instead displays itself either empty or with a check mark if turned on.
CheckToggles can be used for multiple choice or multiple selection input; often, checkToggles are placed into a radioButtonGroup.
Example:

    |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).
Example:

    |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.
Of course, action blocks are also possible:

    |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.

Sliders and Scrollers

Sliders and Scrollers are used much like Buttons, in that either an action is defined to be evaluated when the sliders/scrollers value changed, or a model is informed via a change message.
Sliders and scrollers offer basically the same protocol and act similar. Bothe show a so called thumb which can be moved with the mouse. The only differences is that a sliders thumb is fixed in its size, while a scrollers thumb can change the size.

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:


    |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.
Note:
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.


    |s|

    s := Scroller extent:20 @ 200.
    s scrollAction:[:percent | Transcript showCR:('moved to ' , percent rounded printString) ].
    s thumbHeight:50.
    s thumbOrigin:10.
    s open.
A funny example:

    |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.
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.

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:


    |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.
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).
For example:

    |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.
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 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.
(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).

Scrollbars

Scrollbars are seldom used on their own, you normally do not care for the low-level details (there are complex views, such as 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):


    |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.
But again, this is not the normal use of scrollbars - usually they are connected to some view which calls:

    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.
More on this later ...

Scrolled views

Typically, scrollBars are not created as individual components - they are almost always needed to control another views scrolling position.
To make scrolling easy, ST/X provides a wrapperView class, called ScrollableView, which handles all the geometry and interaction of some view (which is to be scrollable) and the scrollbar(s).
The setup for scrolling is (more details on textViews are found below):

    |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 ...)


    |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
    ]
If you want to have your own view scrolled, use the following code:

    |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: and set an instance variable (viewOrigin) in your view.

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

Miniscrollers

In many applications, horizontal scrolling is a rather uncommon function (for example, since most of the time everything is visible horizontally).
To avoid wasting too much of your valuable screen space, ST/X provides smaller cousins of the scrollers, called MiniScrollers. Both ScrollableView and HVScrollableView can be configured to use miniScrollers instead of full size scrollBars.
This is done in the instance creation message:

    |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.

Textviews

ListView - a view for simple lists

The simplest and most fundamental class to present text is the 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.

Setting the views text

Internally, a listView keeps its text as a collection of strings, each representing a single line of the text. Nil entries are taken as empty lines. You can set a textViews contents with the #list: message, passing a (sequenceable) collection of strings as argument.
For your convenience, there is also a #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.
Examples:

    |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..
However, the "PageUp" and "PageDown" keys (if available on your keyboard) are understood and can be used to scroll your text.

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

or

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).

changing text example:


    |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.

    "/ wait for a while

    Delay waitForSeconds:3.

    "/ remove at top

    1 to:5 do:[:index |
	listView removeIndex:1.
	Delay waitForSeconds:0.5.
    ].
    (Delay waitForSeconds:2).

    "/ remove somewhere in the middle of the visible area

    1 to:5 do:[:index |
	listView removeIndex:5.
	Delay waitForSeconds:0.5.
    ].
    (Delay waitForSeconds:2).

    "/ remove in invisible area - watch the scrollBar

    (listView size) to:30 by:-1 do:[:index |
	listView removeIndex:index.
	Delay waitForSeconds:0.03.
    ].

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:


    |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.
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.

Example using two listViews on the same model, with different listMessages:


    |top model l plainText|

    plainText := #('').

    model := Plug new.
    model respondTo:#modelsUppercaseText
	       with:[ plainText asStringCollection
			  collect:[:l | l asUppercase]].
    model respondTo:#modelsLowercaseText
	       with:[ plainText asStringCollection
			  collect:[:l | l asLowercase]].

    top := StandardSystemView extent:200@200.

    l := ListView origin:0.0 @ 0.0 corner:1.0 @ 0.5 in:top.
    l bottomInset:5; level:-1.

    l model:model.
    l aspect:#modelsAspect.
    l listMessage:#modelsUppercaseText.

    l := ListView origin:0.0 @ 0.5 corner:1.0 @ 1.0 in:top.
    l topInset:5; level:-1.

    l model:model.
    l aspect:#modelsAspect.
    l listMessage:#modelsLowercaseText.

    top open.

    Delay waitForSeconds:3.
    plainText := #('foo' 'bar' 'baz').
    model changed:#modelsAspect.

    Delay waitForSeconds:2.
    plainText := (PipeStream readingFrom:'ls') contents.
    model changed:#modelsAspect.

Scrolling

You will seldom need to scroll a listView directly - typically, they are wrapped into a 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:

conditional scrolling is supported by: Finally, asking for the current scroll position is done with:

TextView - a view for readonly text

Like a 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:

and retrieved with: Example:

    |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 - Editable Text

An EditTextView is similar to TextView, with additional support for editing operations and a textCursor.
Since the text can be modified, 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).
The cursors shape is controlled by the styleSheet - it may be any of a blockCursor, caret or I-beam. Although there exists public protocol to move the cursor programmatically, this is seldom needed except for: The interesting new features are the acceptAction and the modified state:
The acceptAction (if set via #acceptAction:) will be evaluated, and gets the views contents (a collection of lines) as argument.
The modified state keeps track of any changes made to the text: with every modification, is set to true automatically. The textview never clears this flag automatically - it should be cleared by the application, whenever the text is saved, or otherwise considered to be unmodified via the #modified: message.
For example:

    |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 - Streaming into a TextView


An TextCollector is similar to EditTextView, with additional support for streaming operations.
Its main use is for the Transcript.
For example, to create a new transcript stream, which receives the output of the previous example instead of the transcript, try the following.:


    |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.

Workspace - Editable Text plus doIt

A view for editable text which can evaluate expressions. I.e. its basically a view for editable text, with added 'doIt', 'printIt' and 'inspectIt' functions on the popup-menu. The action to be performed on doIt is defined by a block, which can be defined by the owner of this view. (thus you can put a workspace into more complex widgets, and control what should happen on 'doIt'). A useful default action is setup, which simply evaluates the selection as a smalltalk expression. (but, a lisp or prolog view could define its own action ...)
"Try this:"


    |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'.

CodeView - Editable Source Code

A view for text which is known to be smalltalk code. It adds explain to the menu, and defines another action: explainAction to be performed for explain. This action is to be defined by the user of this view (i.e. ususally the owning browser) If used with a model, accept sends the changeMsg to it (as defined in EditTextView). (however, it is possible to define both changeMsg and acceptAction)
"Try this:"


    |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'.

Selection Views

SelectionInListView - Single Selection in a List

A SelectionInListView is a ListView with a selected line, which is shown highlighted.
"Try this:"

	|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.

"Try it with toggling:"

	|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.
A scrollable view can also be added:
"Also with a scrollbar:"

	|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.

MultiSelectionInList - Multiple Selections in a List

If multipleSelectionsOk is true, it is also allowed to shift-click multiple entries.
"For example:"

	|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 - File Selections

A 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.
"Try this simple example:"

	|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."
"A nicer example:"


	|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

"The directories aren't necessary, so let's get rid of them."
"Without the directories:"

	|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:"
"Directories without arrows:"

	|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:"
"A filtered list:"

	|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

"And finally, a more complex filter can be set using the matchBlock method:"
"A complex filter showing only writable file and directories:"

	|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

Dialog boxes

All of the above was about non-modal topviews or views to be used as components for more complex views. Often you need to perform some user dialog, which stops him/her from interacting with the original view. Examples are popup menus, the save-box, the font-selection box and many others.

Standard Dialogs

There is a number of common dialog boxes already available in the system, new ones are easily created by subclassing ModalBox or any other DialogView.

InfoBox - Information Display

The simplest of these DialogBoxes is the InfoBox. It is used to output some information to the user, and stops its caller until the user confirms by pressing a button. Try:

    |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:


    |b|

    b := InfoBox new.
    b title:'how about this ?'.
    b origin:0@0.
    b show
or:

    |b|

    b := InfoBox new.
    b title:'how about this ?'.
    b origin:(Display extent - b extent).
    b show
or, more convenient for the user, with:

    |b|

    b := InfoBox new.
    b title:'how about this ?'.
    b showAtPointer
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).

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:


    |b|

    b := InfoBox new.
    b title:'how about this ?'.
    b showAtPointerNotCovering:(WindowGroup activeGroup topViews first)
Finally, for very urgent information, use:

    |b|

    b := InfoBox new.
    b title:'water in disk !!!!'.
    b showAtCenter
This will open the box at the center of the screen.

By the way: there is a shortcut available for creating AND setting the title of the box:


    (InfoBox title:'wow !') showAtPointer
Also, every object understands another shortcut message: #information:. Which takes the argument as a titletext and opens an info box for it. Thus you can use:

    self information:'this''s simple'
everywhere in your program.

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:


    |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
or:

    |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
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.
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

WarningBox - Warnings

A WarningBox is almost the same as an InfoBox; it simply uses a different (default) icon. See the difference between:

    (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.

YesNoBox - yes/no Confirmations

YesNoBoxes are for simple yes/no questions; they provide two buttons and have actions associated with both of 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).

EnterBox - Entry of a String

EnterBoxes allow the inputting of a string. They will evaluate an actionBlock with the entered string as argument.

    |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).
The above can also be written as:

    |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.

EnterBox2

This is like an EnterBox, but it adds a third button. For example, the search box is of this type.
This box adds another actionBlock, which is evaluated for the additional button, and provides a protocol to set this and the additional buttons label.
Example:

    |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

OptionBox

An OptionBox is similar to an EnterBox, with an arbitrary number of option-buttons. This is a bit more complicated to set up, which is why EnterBox and EnterBox2 have been provided.
See the optionBox's documentation protocol for examples.

A convenient interface (which sets up the box to return a value) is:


    |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.
The returned value could be used with #perform for some real action.

TextBox

A TestBox is similar to an Enterbox, but allows input of more than one line of text.
Actually, this is a tiny little text editor box!

ListSelectionBox

A ListSelectionBox is similar to an enterbox, but offers a list to choose from.

    |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

File Open & Save Dialogs

FileSelectionBox - File Open Dialog
A FileSelectionBox looks like a ListSelectionBox, but the list consists of the file names in a directory.
For example:

    |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.
This makes certain, that the user gets some convenient default directory when the box shows up, i.e. the last directory.

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:


    |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
The FileSelectionBox uses an instance of 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:


    |fileName|

    fileName := Dialog requestFileName:'which file ?'.
    Transcript showCR:'the name is: ' , fileName.
or:

    |fileName|

    fileName := Dialog requestFileName:'which file ?' default:'foo'.
    Transcript showCR:'the name is: ' , fileName.
To ask for a directory name, use:

    |fileName|

    fileName := Dialog requestDirectoryName:'which directory ?'.
    Transcript showCR:'the name is: ' , fileName.
There are a few more of these request methods available - see the DialogBoxes class protocol.
FileSaveBox - File Save Dialog
A FileSaveBox is similar to a FileSelectionBox, with 2 buttons labeled append and save. The action of the new button is defined with appendAction:
as in:

    |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

FontPanel - Choosing a Font

This dialog allows the choosing of a font. The box will evaluate its actionBlock, passing the chosen font as an argument.

    |box|

    box := FontPanel new.
    box action:[:aFont | Transcript showCR:'font is: ' , aFont].
    box showAtPointer

Dialog Compatibility Protocol

Some of the above dialogs are (for historical) reasons implemented by separate subclasses of DialogBox. However, for compatibility with ST-80, dialogBox offers class methods which create and show these dialogs.
You should use messages to Dialog for compatibility with future versions and with other smalltalk systems.
The corresponding messages are
for notifications:

    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'.

Custom Dialogs

 Text about how custom dialogs are created using the Dialog class
 to be added here.

 For now, see examples in DialogBox's documentation category.

Popup Menus

PopUpMenus are typically defined with an array of label-strings and an array of selectors. When activated, a message with a selector corresponding to the selected entry will be sent to some object.
The simplest way of defining a popupmenu is:

    |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

Defining a Middle Button Menu

Usually, popUpMenus are associated with the middle-mouse button. The handling of this is done in some superclass of all views, so you normally do not have to care about the details of opening and closing these menus.
There are two principle ways to define a views menu: The next examples use static menus, since these are easier to handle in these isolated examples. We will learn how dynamic menus are handled in a minute ...

Try the following:


    |myView myMenu|

    myView := View new.
    myMenu := PopUpMenu
		    labels:#('foo' 'bar')
		    selectors:#(doFoo doBar)
		    receiver:myView.
    myView middleButtonMenu:myMenu.
    myView open
(if you try this example, be prepared to have a debugger come up - the view will of course not understand any foo-bar messages.
Simply press 'continue' or 'abort' to leave the debugger)

For a working example, try:


    |v m|

    v := View new.
    m := PopUpMenu
	    labels:#('lower'
		     'raise'
		     '-'
		     'destroy')
	    selectors:#(#lower #raise nil #destroy)
	    receiver:v.
    v middleButtonMenu:m.
    v open
This example also shows how grouping lines are added to a menu. The item labels '-' 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:


    |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.
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 'ignored' 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:
	    args:#(1 2 3)
	    receiver:nil.
    v middleButtonMenu:m.
    v open.

Check-Mark Entries

It is also possible, to add check-mark entries, with an entry string starting with the special sequence '\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: as demonstrated in the following menu:

    |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.
Currently, these special sequences cannot be used as plain item labels (you have to add a space or any other character to trick the menu)

Wrapping Arbitrary Views as Popups

You can wrap any other view into a popup menu (for example, to implement menus with icons or other components). The wrapped view should respond to some messages sent from popupmenu (for example: #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.


    |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 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).
    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
or even (see below for more on submenus):

    |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
You will find some more examples in the PatternMenus class documentation.

Static menus can be used with any other view - the following adds one to a button:


    |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

The buttons left-mouse-button functionality is not affected by the added middle-button menu.

BTW: this is how PopUpList is implemented.

ST-80 Style Menus

The above menus all did some message send on selection; it is also possible, to use Smalltalk-80 style menus (which return some value from their startup method):

    |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.

Defining Submenus

Submenus are created by changing an entry using #subMenuAt:put:.
For example:

    |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).

Dynamically adding/removing Menu Entries

You can get the index of an existing entry with:

	aMenu indexOf:someKey
where key can be a selector or an entries text (again use the selector).
Then, add new entries with:

	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.

Dynamic Menus

In most of the previous examples, menus were statically set via the #middleButtonMenu: message.
In contrast, dynamic menus are created on demand - actually they are created at button press time, and can therefore be easier adapted to the current state of the view (often: on the current selection).
To do this, there must be some instance which provides the menu - this is called the menuHolder. By default (if not set otherwise), a views menuHolder is its model, or, if it has no model, its the view itself.
(however, a subclass of 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;


    |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
of course, the model should respond to #foo and #bar; to make things more interesting, lets make the models menu response dependent on some internal models state:

    |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
The above was a somewhat simple example, in that the view has no builtin default menu.
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:

    |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
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)
Setting the menuPerformer, we get:

    |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
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).
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

Pulldown Menus

PullDownMenus are created as a subview (typically) in a StandardSystem view:

    |topView menu|

    topView := StandardSystemView new.
    menu := PullDownMenu in:topView.

    topView open
it sets its origin and extend to some useful default (full width);
however, modifications are possible, as in:

    |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:


    |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
to access the items further, each one should be associated with some key; this is done with the selectors: message:

    |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
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).

    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
the individual submenus are instances of 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

Special views

ShadowView - shadows under views

this section is to be written

InputView - a transparent (input-only) view

Instances of InputView are transparent, and can be used over another view to catch its events. InputViews are typically used as a plugged view; i.e. they are usually configured to forward events to some other view.
An example use can be seen in the GUI Painter, which uses an inputView over the construction (drawing) area, to keep the widgets shown there from getting events.

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.

Arbitrary shaped views

You can give your views arbitrary (non rectanglular) shapes if your graphic system supports this (i.e. if your X-server supports the Shape Extension). The use of this feature is tranparent: on systems which do not support arbitrary borders, the views will be rectangular, and the shape definition is silently ignored.

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:


    |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
For portable applications, you should not use viewShapes, since not all X servers support this.
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.

The root view

this section is to be written

The Display

this section is to be written

Using modal boxes non-modal and vice versa

It is possible to startup any view as a modal box, and to open dialogs in non-modal mode:

	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.

Other views

this chapter is to be written

Rulers

this section is to be written

ObjectView - for structured graphics

The 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.
The whole graph usually consists of multiple (possibly overlapping) such elements, and ObjectView knows how to correctly redraw such a graph, how to select and highlight elements, to scroll and zoom-in/out.
In addition, it provides hooks for actions (keyboard input, button press or doublePress) to be forwarded to elements.

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.

To load this class and open a browser on it, press:
this section is to be written

Creating your own views

this chapter is to be written

Model-View-Controller operation again

this section is not yet finished - please use the browser to see more details.

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].

Buttons, Toggles etc. with a model

To have a button operate on a model, you have to Example:

    |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.
Example:

    |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.
If you set the changeSeletor to a one-argument selector, the button will pass the current state as additional argument (for buttons, this will always be true, but for toggles or radioButtons, this might be false as well).
Example:

    |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.
Example:

    |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.


    |t m|

    m := ValueHolder with:false.

    t := Toggle on:m.
    t label:'press me'.

    t open.
    m inspect
the last statement opens an inspector on the valueHolder; see the field named value and how it changes when the toggle changes state.

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:


    |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.
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.

Example: creating two toggles on different valueHolders


    |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
Models inform their dependents about value changes; valueHolders send a "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

EditFields with a model

Having an editField operate on a model is very similar to the above button examples:

    |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.
The value is accepted, if any of the following occurs: The following is a typical setup for an enterField in a dialogBox:

    |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.

TextViews with a model

The model has to provide a method which returns the text to be displayed; this is called the listMessage. The TextView will automatically (re)fetch the text via above message, whenever it gets a change notification about a change of the aspectSymbol.
If the text is editable (i.e. the view is an editTextView or subclass), you also have to define the selector of a message which is sent whenever the text is accepted (the changeMessage).
Example (non editable):
(Notice, using a plug here for demonstration only; in real world applications you would use an instance of a 'real' model)

    |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.
Example (editable text):

    |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.

SelectionInListViews with a model

The setup is:

    |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.
To acquire the list of selectable entries, the selectionInListView will send #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.
Whenever the selection changes, the selectionInListView will send the change-message to the model. As in the above button example, this may be a zero, one or 2-arg selector. If its a one or 2-arg selector, the selected entries value will be passed as first argument. The passed value will be the entry from te list which was optained via the #getList message - except, if useIndex was true (as in the above); in this case, the numeric index in the list is passed instead.
Finally, #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

Complex models

The above examples mostly used ValueHolders as simple models. In real life applications, models are often more complex, consisting of many fields.
Consider the case of a personal record, with fields for firstName, lastName, date of birth and so on.
If you used the above valueHolders for all fields, you had to extract all fields values from your person object, put them into value holders, open a dialog on those value holders, and store back the fields from the valueholders back into the person object.
This can be avoided by two different mechanisms: choose whichever you prefer; the following shows examples for both:
(notice: since we do not have a Person class in the system, we use a Plug for the demonstration. Plugs implement messages by evaluating a corresponding block - thereby allowing easy simulation of other objects protocol. In your real application, you would have an instance of a Person class, of course)
Using adaptors:

    |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 ....

Applications and TopView Control

Window Modes

Windows can be opened basically in two modes:

Modeless Windows

The modeless mode is the normal mode. For example, if you open a new workspace in the launcher, that window (actually: that windowGroup) will be operated in modeless mode: a new windowGroup is created, which contains the topView and all of its child views, and a separate window process is started to serve its events. Basically, such applications/windows are started like:

    |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.

Modal Windows

The modal mode is used for dialogs and popup windows, which behave similar with regard to how the control is taken from the caller while being shown, but are different in their window decoration (popups do not have a window decoration with label and resize, close and maximize controls).

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:


    |app|

    app := WorkspaceApplication new.
    app openModal.
Notice, that the caller is blocked until the window is closed.

Modeless without Decoration

Use 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.

Controlling Multiple TopViews

Often, applications consist of multiple topViews, which are to be controlled as a unit by the window manager with respect to iconification and closing. With the help of the windowGroup object, multiple views can be connected to form a unit.

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:


    |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).
In the above, the slaveView is automatically closed when the master is closed. However, the master is not affected by the slave being closed.
For a mutual partnership, use #bePartner, as in:

    |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).
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 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).
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.

In the next example, the slave is closed whenever any of its masters is closed - but those do not affect 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).
The following closes the slave with any of the two masters, which are partners (i.e. control each other):

    |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).
Since modalBoxes can also be opened modeless, controlPanels for applications can be created with this mechanism:

    |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.
Notice:
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:


    |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).
the same using partners:

    |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).

Flushing events for an application

You can flush (or otherwise manipulate) events for an application, since all of its views get their events via a single instance of 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 ....

Smalltalk/X's drawing model

The following chapter tells you how the low level drawing is done. If you use the above widgets, you do not need to know these details; they do all the drawing for you.

Geometry Objects & Wrappers

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.
Geometric classes are provided for lineSegments, polygons, splines, curves, arcs, ellipses, circles and arrows. Wrappers are available to draw those either filled or unfilled (stroked).

Here is an example:


    |topView p pie pie2 arc rect spline1 spline2|

    topView := StandardSystemView label:'Geometric Objects'.
    topView extent:400@400.
    topView openAndWait.

    topView translation:10@10.

    p := Polygon vertices:{ 10@20 . 100@20 . 50@60 }.
    topView addComponent:(p asFiller:Color red).

    pie := EllipticalPieSlice center:150@150 radius:60 startAngle:45 sweepAngle:90.
    topView addComponent:(pie asFiller:#blue).

    pie2 := pie translatedBy:50@150.
    pie2 := pie rotatedBy:20 degreesToRadians about:pie2 center.
    topView addComponent:(pie2 asFiller:Color orange).

    arc := EllipticalArc center:150@150 radius:60 startAngle:45 sweepAngle:90.
    topView addComponent:(arc asFiller:#green).

    rect := Rectangle origin:250@250 extent:50@70.
    topView addComponent:(rect asFiller:
			    ((GradientBackground
				vertical:(Color red) to:(Color blue))
				    usedLength:200)).

    topView addComponent:((LineSegment from:(0@-90) to:(0@90)) asStroker:#lightGrey).
    topView addComponent:((LineSegment from:(-90@0) to:(90@0)) asStroker:#lightGrey).

    spline1 := Spline controlPoints:{200@20 . 250@50 . 220@80}.
    topView addComponent:(spline1 asStroker:#magenta).
    spline2 := (spline1 translatedBy:10@100) rotatedBy:-45 degreesToRadians about:200@150.
    topView addComponent:(spline2 asFiller:#cyan).
then resize or cover the view to see that it redraws its objects automatically.

Low Level Drawing

Low level draw commands are more or less directly forwarded to the underlying graphic system (X11 or Windows).

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.
Therefore,

	aGC paint:(Color red).
	aGC displayLineFrom:(10@10) to:(50@50).
will draw a red line.

Demo View Setup

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:


    |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.
then draw into it with:

    DemoView paint:(Color black).
    DemoView displayLineFrom:(0@0) to:(50@50).
to clear the view, use:

    DemoView clear
Once you are finished with these examples, close the view and remove the workspace variable with:

    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:


    DemoView viewBackground:(Color yellow).
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.
If you want to make the new viewBackground immediately visible, you have to use:

    DemoView viewBackground:(Color blue).
    DemoView clear.
You can use either a color or an image as viewBackground:

    DemoView viewBackground:(Image fromFile:'goodies/bitmaps/gifImages/garfield.gif').
    DemoView clear.

Coordinates

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.

The paint color

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.

Drawing lines:


    DemoView clear.
    DemoView paint:(Color white).
    DemoView displayLineFrom:(0@0) to:(50@50).
or (if you have x/y coordinates available, and want to avoid the creation of temporary points):

    DemoView clear.
    DemoView paint:(Color white).
    DemoView displayLineFromX:50 y:0 toX:0 y:50.
also, vector representation (polar coordinates) is sometimes handy:
(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.

Drawing rectangles:


    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, ellipses & circles:

Arcs can be drawn -
by specifying a bounding box:


    DemoView clear.
    DemoView paint:(Color green).
    DemoView displayArcX:0 y:0
		   width:50 height:50
		    from:0 angle:180
or a center-point and radius:

    DemoView clear.
    DemoView paint:(Color white).
    DemoView displayArc:(25@25)
		 radius:25
		   from:180 angle:180
if the bounding box is not square, you get (part of) an ellipse:

    DemoView clear.
    DemoView paint:(Color green).
    DemoView displayArcX:0 y:0
		   width:75 height:25
		    from:0 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:360
or:

    DemoView clear.
    DemoView paint:(Color green).
    DemoView displayArcX:0 y:0
		   width:50 height:50
		    from:0 angle:360
for full circles, there is a shorter method available:

    DemoView clear.
    DemoView paint:(Color red).
    DemoView displayCircleX:25 y:25 radius:25
or:

    DemoView clear.
    DemoView paint:(Color red).
    DemoView displayCircle:(50@50) radius:25

Polygons:


    |p|

    p := Array with:(10@10)
	       with:(75@20)
	       with:(20@75)
	       with:(10@10).
    DemoView clear.
    DemoView paint:(Color magenta).
    DemoView displayPolygon:p

Strings:


    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).
the current font is accessed via aView font.
Therefore, multiline text is drawn with:

    |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:


    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
of course, the angle is not limited to multiples of 90:

for a demonstration, resize the demoView:


    DemoView topView extent:400@480.
then draw strings with:

    |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
    ]
(the lines are drawn to show the baselines of the strings.)

Bitmaps:


    |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:


    DemoView clear.
    DemoView paint:(Color red).
    DemoView fillRectangle:(1@1 corner:50@50).
Filled arcs, ellipses & circles:

    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
or:

    DemoView clear.
    DemoView paint:(Color green).
    DemoView fillCircleX:25 y:25 radius:25
Filled polygons:

    |p|

    p := Array with:(10@10)
	       with:(75@20)
	       with:(20@75).
    DemoView clear.
    DemoView paint:(Color magenta).
    DemoView fillPolygon:p
Have a look at the GraphicsContext-class for even more drawing methods.

Opaque drawing

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:

Drawing opaque strings:

    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
Drawing opaque bitmaps:

    |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.
Thus, you can create transparency effects as in:

    |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:


    |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.
or:

    |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.
(see a more detailed description of joinStyle below).

of course, filling works too:


    |pattern|

    pattern := Image fromFile:'libwidg3/bitmaps/woodH.tiff'.

    DemoView clear.
    DemoView paint:pattern.
    DemoView fillCircle:(50@50) radius:25.
the same is true for 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
opaque strings:

    |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
another opaque string:

    |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
finally, an opaque string with both fg and bg being patterns:

    |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
and bitmaps:

    |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
or:

    |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
opaque bitmaps (foreground is a pattern, background a color):

    |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 (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:Color yellow on:pattern.
    DemoView displayOpaqueForm:bits x:0 y:0
or even (both foreground and background are patterns):

    |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

Line styles

In the above line, rectangle, polygon and arc examples, we were drawing solid lines. You can also draw dashed lines:


    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 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:#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.
the default (if not specified otherwise) is solid:

    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.
Line width

You can set the lineWidth with #lineWidth:. The argument is width of the line in pixels.


    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.
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.
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.

Join style

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:


    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.
and:

    DemoView clear.
    DemoView paint:(Color yellow).

    DemoView joinStyle:#round.
    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:#miter.   "/ thats the default anyway
    DemoView lineWidth:10.
    DemoView displayPolygon:(Array with:10@10
				   with:80@10
				   with:45@80
				   with:10@10)
compare to:

    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)
or:

    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)
Cap style

The capStyle controls how the endPoints of individual lines are to be drawn:


    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.
or:

    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.
a special capStyle is #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:


    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).
This inverts triangle correctly:

    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).
Notice:
Some displays have problems with wide lines (lineWidth > 1), and either ignore the cap-not-last setting, or display a wrong picture.
You should only use this for thin lines (or blame it on the display vendor ;-).

Transformations

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:


    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.
everything will be transformed; even strings, bitmaps and line widths:

    |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.
(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)

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:


    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 same in inches:

    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.
    ]
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.
For this, you can use displayUnscaledString:x:y: which transformes the x/y coordinate, but draws the unscaled (device) font.

    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.
    ]
There are also unscaled versions of the opaque string methods and the bitmap display methods.

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.

Colors

Instances of 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: Finally, on displays which use a color lookup table, writable color cells can be allocated with:

    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).
Always write your application to provide a fallback algorithm (for example, by doing full redraws) in case the display server does not use a color lookup table.
(ask the display for its capabilities, using Display visualType.)

Fonts

Instances of Font represent fonts in a device independent way. Internally, they store the name of the font as family, face, style and size.
Instances are created by:

    |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.
Therefore a size 12 font may (and will) have a different number of device pixels depending on the medium on which it is to be rendered. On a 100dpi display, it will be about 12*(1/72)*100 = 16 pixels high; on a 75dpi display, about 12 and on a 300 dpi printer page, there will be roughly 12*(1/72)*300 = 48 pixels. The device is free to round, or take a nearby size as replacement. You should make your applications drawing independ if the physical size.

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.
The conversion from a device independent font to one that is bound to a specific device is done by sending "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).

Cursors

For cursors, the same device-independent vs. device dependent story is true as for fonts or colors. However, this conversion is typically done automatically by the views cursor setting methods.

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).
Thus, a busy subview can change the cursor of all its relatives with (assuming self is some view):

    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.
Beside windowGroups, views also understand this message:
(assuming self is some view)

    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.

Example:


    |top v1 v2 v3 v4|

    top := StandardSystemView extent:200@200.
    v1 := View origin:0.0@0.0 corner:0.5@0.5 in:top.
    v1 level:-1.
    v2 := View origin:0.5@0.0 corner:1.0@0.5 in:top.
    v2 level:-1.
    v3 := View origin:0.0@0.5 corner:0.5@1.0 in:top.
    v3 level:-1.
    v4 := View origin:0.5@0.5 corner:1.0@1.0 in:top.
    v4 level:-1.

    v1 cursor:(Cursor hand).
    v2 cursor:(Cursor extent:16@16
		      sourceArray:#(
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000
				2r0000000110000000
				2r0000000110000000
				2r0000001111000000
				2r0000111111110000
				2r0000111111110000
				2r0000001111000000
				2r0000000110000000
				2r0000000110000000
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000 )
		      maskArray:#(
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000
				2r0000000110000000
				2r0000000110000000
				2r0000001111000000
				2r0000111111110000
				2r0000111111110000
				2r0000001111000000
				2r0000000110000000
				2r0000000110000000
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000
				2r0000000000000000 )
		      offset:-8@-8).
    v3 cursor:(Cursor fromImage:(Image fromFile:'goodies/bitmaps/xpmBitmaps/cursors/cross2.xpm')).
    v4 cursor:(Cursor normal).
    top openAndWait.

    Delay waitForSeconds:5.
    top withCursor:Cursor wait do:[10000 factorial].

Events

The following is only of interest, if you plan to create your own widgets or view classes.
Every event is initially handled by an event dispatcher process, of which there is exactly one per display screen.
(As described below, you can handle multiple screens, by starting an event dispatcher and create views on the other display(s)).

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.

Event types

The following table lists (the subset of the most interesting) events and the corresponding messages which are sent to the view:

Keyboard Events

Keyboard event information constist of the keyCode and the x/y position of the mouse pointer at the time of the event. The keycode can be a character for normal keys or a symbol such as #Copy, #Cut etc. for Acceleration keys.

Moude Events

For mouseButton press and release events, the information constist of the button number and the x/y position of the mouse pointer at the time of the event.

For motion and enter/leave events, the button-state is encoded as bits in the buttonState argument, which is an integer number.

Redraw and Visibility Events

Views Configuration

Other events

A special situation arises for events which pass x/y coordinates, and the view has a (non-identity) transformation defined:
In this case, some of the above methods are called with logical coordinates (i.e. the inverse transformation being applied). If the view is interested in the raw device events (i.e. device coordinates), it should redefine the deviceXXX methods, which actually do the transformation and call for one of the above methods with the logical coordinates.
For example, to get the device coordinate for buttonPress events, redefine deviceButtonPress:x:y: in your view class.

See the classes WindowSensor, WindowGroup and WindowEvent for more (internal) details.

Enabling/disabling events

some special events have to be enabled explicitely - they are ignored by default.
Events are enabled with: and disabled with: Key, button, focus and exposeEvents are always enabled by default (but can be disabled and reenabled).

See the enableXXX and disableXXX methods in the PseudoView class for more details.

Notice:
Not all event types will be delivered with some display devices - some (like colorMap or selection notifications) may be very device specific.
Enable/disable messages for those unimplemented events will be silently ignored.

Event forwarding: delegates and controllers

Events can be forwarded to a delegate. Any view with an event delegate will not receive user events in which the delegate has shown interest. Instead, these will be forwarded to the delegate with the original view as an addition argument.
To delegate events, you need some object which understands one or more of the messages: and set the views delegate with:

    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: If the delegate does not implement those messages, this is taken as if it is not interested in those events. Therefore, only the 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.)

Sending events to other (alien) views

On some display station types (currently: X displays only), it is possible to send keyboard events to other (possibly alien) views.
A scenario, where this makes sense is to provide input to an inputField of another application, which does not correctly support the clipboard mechanism.
Low level events are sent with:

    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:


      |point id|

      point :=  Display pointFromUser.
      id := Display viewIdFromPoint:point.
      Display simulateKeyboardInput:'Hello_world' inViewId:id
This is a very special functionality and only recommended to help interfacing to badly written alien applications.

Event listening

For certain types of applications (event recorders, automatic help messages etc.) it may be useful to catch any event, for any known view in the smalltalk system.
(In theory, this could be acomplished using delegates for every single view - but this is too complicated to set up and manage.)
To allow for this, the 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.
Once registered, the event listener will get the same type of messages as listed for the delegates in the previous section.

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)
with:

    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.

Flushing events

In some situations, it may be required to remove pending events from a sensor's input queue.
A situation where this is useful is to remove typeahead user input, in case of some error situation.
The following flush methods are available:

Handling multiple screens

Smalltalk/X's combination of event and process driven event handling allows the system to serve multiple display screens; in fact, the setup required to implement applications doing this is pretty easy. (especially, due to the way, colors, fonts etc. are handled).
Of course, handling multiple screens is not possible with all types of display: a Windows or OS/2 Display does not support remote access; therefore, the things described below are (currently) only valid for Xserver based implementations.

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:

Thats it; this even allows a team of programmers to work concurrently in different browsers (on different screens) within the same executing smalltalk system (or your data entry application to handle multiple Xterminals).

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:


    Display2 keyboardMap:(Display keyboardMap).
    Display2 buttonTranslation:(Display buttonTranslation).
before opening the first views on the other display. Additional background info:
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.

Notice:
If an image is restarted, which was saved while multiple screens active, it will (currently) not automatically reconnect to all displays and reopen those remote views.
Your application has to do this manually, by making itself a dependent of 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.

Notice2:
Although it looks pretty cool, to have multiple systemBrowsers open on multiple displays, be reminded that the system has no provisions to handle concurrent access to the class hierarchy.
With the current version, it is not recommended to use multiple display connections for group development.

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).

Notice3:
If multiple launchers are open, there are now multiple transcript windows, where informational output can be sent to: The same caution as with accessed to the 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.

Trouble Guide

This chapter tries to list common errors made in view programming. It lists trouble symptoms and gives hints for fixing.
The list is defintely not complete and will be extended over time (as trouble reports arrive from users). Please help others, by telling us about your problems (even if you found fixes) for inclusion into this list.

Notes:
(*) subview creation
If you read the sections on color, font and cursor handling, you may notice that there is a slight difference: if a view is created without a superview, its physical device is not known until the subview is placed into a topview (because the topview could be on any graphics device).
Therefore, certain queries for device specific (such as color-index, pixel sizes of fonts etc.) are not possible until the subview is placed into its topView.
For most normal applications, this does not make a difference. However, for example, if you want to fix a subviews dimension based upon its font height before that view is placed into a topView, the font size query will fail and report an error.


ST/X Logo Copyright ? 1995 Claus Gittinger, all rights reserved

<cg at exept.de>

Doc $Revision: 1.78 $ $Date: 2021/03/13 18:24:52 $