3.4 Composing

What we want is a direct handle on the ruler to rotate it. We can achieve this with a button morph inserted somewhere on the ruler.

insertButtons
| btn |
btn := ButtonMorph model: self action: #rotateRuler ::
   actWhen: #buttonStillDown;
   icon: Theme current refreshIcon;
   color: Color transparent;
   selectedColor: Color yellow darker;
   morphExtent: Theme current refreshIcon extent * 1.5.
self addMorph: btn.
btn morphPosition: length * self ppcm @ 30

The button morph added to the ruler is positioned according to the coordinate system of the ruler – its owner. Whenever it is scaled or rotated later with its halo handles, its sub-morphs – here the buttons – are positioned accordingly. We’ll place the button at the end of the ruler, as this is more practical when pivoting the ruler. The button acts when the user holds down the mouse button; the method rotateRuler is then called.

In this method, we calculate the angle between two consecutive vectors from the ruler’s rotation center to the mouse positions. As the mouse positions are in world coordinates, we need the ruler’s rotation center – also its origin – to be converted into the world coordinate system – self externalizeToWorld: self rotationCenter.

As we want both the angle and the direction of the user’s gesture – upward or downward – we need a signed angle. Therefore, we use the vector product7 to deduce the angle between these two consecutive vectors. This angle is then used to rotate the ruler accordingly.

rotateRuler
| event p1 v1 v2|
"anything new to do?"
event := self activeHand lastMouseEvent.
event isMove ifTrue: [
   p1 := self externalizeToWorld: self rotationCenter.
   v1 := lastHandPosition - p1.
   lastHandPosition := event eventPosition.
   v2 := lastHandPosition - p1.
   (v1 isZero or: [v2 isZero]) ifTrue: [^ self].
   self rotateBy: ((v1 crossProduct: v2) / (v1 r * v2 r)) arcSin ]
ifFalse: [lastHandPosition := event eventPosition]

You may want to take a second look at this method; it is a bit complex at first, but it also exposes the wonderful design of Cuis-Smalltalk’s Morph 3, which makes this type of user interaction quite easy to manage.

ch03-rulerRotateButton

Figure 3.9: A handy button to rotate the ruler

Our next move is to give the user the ability to change the length of the ruler, the length attribute. Therefore, we insert a second button to do so and, in the process, refactor the method:

insertButtons
| btn buttonExtent |
buttonExtent := Theme current refreshIcon extent * 1.5.
btn := ButtonMorph model: self action: #rotateRuler ::
   actWhen: #buttonStillDown;
   icon: Theme current refreshIcon;
   color: Color transparent;
   selectedColor: Color yellow darker;
   morphExtent: buttonExtent.
self addMorph: btn.
btn := ButtonMorph model: self action: #resizeRuler ::
   actWhen: #buttonStillDown;
   icon: (Theme current fetch: #( '16x16' 'actions' 'go-last' ));
   color: Color transparent;
   selectedColor: Color yellow darker;
   morphExtent: buttonExtent.
self addMorph: btn.
self positioningButtons 

As the length varies, so does the morph’s width. Therefore, it makes perfect sense to have a distinct method to move the buttons into the right position:

positioningButtons
| buttonWidth position |
buttonWidth := submorphs first morphWidth.
position := length rounded * self ppcm -4 @ 30.
submorphs do: [:btn |
   btn morphPosition: position.
   position := position translatedBy: -4 - buttonWidth @ 0 ]

Now, the resizeRuler method to effectively change the length of the ruler looks a bit similar to rotateRuler in its general shape. But there are subtle differences to benefit from the Morph 3 design.

resizeRuler
| event prev |
"anything new to do?"
event := self activeHand lastMouseEvent.
event isMove ifTrue: [
   prev := lastHandPosition.
   lastHandPosition := self internalizeFromWorld: event eventPosition.
   self length: length + (lastHandPosition x - prev x / self ppcm)]
ifFalse: [lastHandPosition := self internalizeFromWorld: event eventPosition]

When the user holds down the resize button, the ruler should shrink when the mouse pointer moves in the direction of the smallest value of the ruler’s x-axis and its length should increase when the mouse pointer moves in the direction of the greatest value of its x-axis.

ch03-rulerCoordinateSystem

Figure 3.10: The ruler and its coordinate system exposed

To determine these behaviors of the mouse pointer, the pointer’s coordinates – expressed in the world coordinate system – must be converted into the local coordinate system of the ruler. This is exactly what is done by internalizing the mouse position self internalizeFromWorld: event eventPosition. Then, the abscissa delta between the two last mouse positions is calculated to determine the change in the ruler’s length. And because this calculation is performed in the ruler’s coordinate system, it works regardless of its pivoted state.

ch03-rulerResizeButton

Figure 3.11: A handy button to resize the ruler

Of course, when the length is adjusted, the positions of the buttons are recomputed and the ruler is flagged for redrawing:

length: newLength
length := newLength max: 1.
self positioningButtons.
self redrawNeeded 

Because the drawOn: expects an integer value for the length, a method variable roundedLength is set to its rounded value and used instead in the rest of the method:

drawOn: canvas
| font grad posX extent step roundedLength |
font := FontFamily familyName: FontFamily defaultFamilyName pointSize: 8.
roundedLength := length rounded.
extent := roundedLength * self ppcm + (self ppcm / 2) @ 60.
../..

Our journey in the world of Morph ends here. We hope this booklet gives you enough insight and know-how to deepen your understanding and start your own project. Developing small or large projects with the Cuis-Smalltalk system and its Morph 3 framework is very gratifying; we hope you have fun in your project!


Footnotes

(7)

https://en.wikipedia.org/wiki/Cross_product#Definition