6.2 Easing GUI design

Let’s star the exploration with a few components to ease the creation of a GUI.

6.2.1 Label that squeeze

Sometime, label may have a tendency to occupy more place than available. It becomes particularly true when you do not control the content of a label when an application is translated in other languages, with a more or less verbose way to express messages or concepts.

The SqueezeLabelMorph tries its best to contract a message in a given amount of character. It is part of the UI-Core.pck.st package, in a Workspace install it by executing the appropriate command:

Feature require: 'UI-Core'

This kind of label is set with a minimum number of characters it is willing to display. If that minimum number is lower than the label’s content, it will contract the text:

(SqueezeLabelMorph 
   contents: 'I am a very looong label with maybe not enought place for me'
   minCharsToShow: 20) openInWorld.

Example 6.1: Label that squeezes

The content of the label is very long, particularly as we inform the label accepts to be squeezed to a minimum of 20 characters. Observe how such squeezed label reveals its complete content in a balloon text when the pointer is hovering it.

ch06-squeezeLabel

Figure 6.1: A label squeezed to 20 characters

When more space is made available to the label, more text of its content is revealed:

ch06-squeezeLabelMore

Figure 6.2: A squeezed label given some more space

When packing a SqueezeLabel in a layout morph with other morph, it will have consequence on the minimal width of the owner layout.

Compare the two examples with a squeezed and regular label:

| label row | 
label := SqueezeLabelMorph 
	contents: 'I am a very long label'
	minCharsToShow: 15.
row := LayoutMorph newRow.
row
   addMorph: label;
   addMorph: (TextModelMorph withText: 'some input' :: morphExtent: 100@0).
row openInWorld

Example 6.2: Squeezed label for a text entry

The whole layout is contracted to a smaller width

ch06-squeezeLabelLayout

Figure 6.3: A text entry with a squeezed label

when comparing to a regular label use case

ch06-labelLayout

Figure 6.4: A text entry with a regular label

| row | 
row := LayoutMorph newRow.
row
   addMorph: (LabelMorph contents: 'I am a very long label');
   addMorph: (TextModelMorph withText: 'some input' :: morphExtent: 100@0).
row openInWorld

Example 6.3: Regular label for a text entry

It is up to you to decide between the compactness of the GUI and the readability of the labels.

6.2.2 One line entry

In Text entry, we presented a quite complex and feature complete class to handle multiple line of text editing. When only one line editing is needed it is a bit overkill, in that circumstance you can alternatively use the TextEntryMorph, part of the UI-Entry package:

Feature require: 'UI-Entry'

This class is quite simple and contrary to the TextModelMorph it does need a text model. Therefore there is no such things as changed and update mechanism involved, it is a passive morph.

However, it offers two options to interact with other objects:

  1. Send it the message #acceptCancelReceiver: to attach an object answering to the #accept and #cancel messages when the Enter or Esc keys are pressed.
  2. Send it the message #crAction: to set a block of code, with no argument, to be executed when the Enter key is pressed.

Let’s experiment with the associated object answering to the #accept and #cancel messages. We need a TextEntryDemo class:

Object subclass: #TextEntryDemo
   instanceVariableNames: 'value entry'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'DesignGUI-Booklet'

At initialize time we create all the needed objects:

initialize
value := '42'.
entry := TextEntryMorph contents: value.
entry acceptCancelReceiver: self.
entry openInWorld

Now we make our TextEntryDemo to respond to the #accept and #cancel messages.

When pressing Enter, we update our value attribute

accept
value := entry contents.
'I accepted the input ' print.
('value = ', value) print

but when pressing Esc we just delete the morph

cancel
'I discarded the input' print.
entry delete

Observe we only need the accessors #contents/#contents: to set and retrieve its content. It is a very simple class to use. If dependency mechanism where to be needed, an intermediate object as the TextEntryDemo can still be used with the observer pattern.

6.2.3 Labelling widget

In example of text entry, we use layout to associate a text entry with a label. It is something very common when building a GUI, the LabelGroup does exactly that for an arbitrary number of morphs.

Feature require: 'UI-Widgets'

When creating a LabelGroup, we associate labels and widgets/controls in an unique group. In return the user gets a layout to be inserted in a dialog or a window.

(LabelGroup with: {
'First Name:' -> TextEntryMorph new.
'Last Name:' -> TextEntryMorph new}) openInWorld

Example 6.4: Labelling a group of morphs

ch06-labelGroup

Figure 6.5: Text entries associated with labels

The group also gives access to the controls, although it is not a very efficient way to access the input widgets used in the group, it is handy:

ch06-labelGroupControls

Figure 6.6: Access to the controls of a label group

A label group is useful when constructing small dialog, in the next section we build one with the morphs we learnt in this section and the previous ones.

6.2.4 Packing in Panel & Dialog

Small window the user interact with are called dialog or panel, Cuis-Smalltalk-UI offers several alternatives to use.

Feature require: 'UI-Panel'

Let’s rewrite the example of text entry with what we just learnt. The end result will look like this:

ch06-greetingDialog

Figure 6.7: A greeting dialog

In the hierarchy provided by the UI-Panel package, we use the DialogPanel class. It offers both an area to plug our interactive components and an area for our button.

DialogPanel subclass: #GreetingPanel
   instanceVariableNames: 'firstName lastName greetLabel'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'DesignGUI-Booklet'

We set the default color of the dialog

defaultColor
^ `Color lightOrange`

then install the iconic buttons for its title

initialize
super initialize.
self showButtonsNamed: #(close expand)

To know about the available buttons for the title bar of a panel, read the class WindowTitleMorph. The expand action needs a rewrite of its associated action

expandButtonClicked
self fullScreen

We set our components in the dedicated newPane method of the Dialog hierarchy:

newPane
| column group |
column := LayoutMorph newColumn :: 
   color: Color transparent;
   gap: 10 .
group := LabelGroup with: {
   'First Name: ' -> (firstName := TextEntryMorph contents: '') .
   'Last Name: ' -> (lastName := TextEntryMorph contents: '') }.
greetLabel := LabelMorph contents: '' font: nil emphasis: 1.
column 
   addMorph: group layoutSpec: (LayoutSpec fixedWidth: 300);
   addMorph: greetLabel layoutSpec: (
      LayoutSpec proportionalWidth: 0 fixedHeight: 20 offAxisEdgeWeight: #center).
^ column

The button has its own method too for installation:

newButtonArea
^ PluggableButtonMorph model: self action: #greet label: 'Greet' ::
   color: self widgetsColor

Finally we implement the greet action of the button to update the greetLabel:

greet
greetLabel contents: (
   'Hello {1} {2}!' format: {firstName contents. lastName contents})