Morphic

Mikael Kindborg
mikki@ida.liu.se
IDA/HCS
2006-10-30

Introduction

Warning: Please note that most code examples that follows below are not complete programs, and that the examples are not fully tested.

Initialize

When creating a morph instance the method initialize gets called. If you overide this method be sure to call the superclass method, or you are lost!

Example:

initialize
    super initialize.
    self color: Color orange.
    self extent: 200@200.
    self borderWidth: 2.
    self borderColor: Color black.

Morphic structure

Messages for working with the morph structure:
m1 openInWorld. "or World addMorph: m1"

m1 addMorph: m2.
m1 addMorphFront: m3.
m1 addMorphBack: m4.

m4 owner submorphs do: [:m | m delete].
Adding a morph to another removes the morph from its former owner.

World is a global variable that refers to the world.

Positioning morphs

All morphs use the global coordinate system.

Several messages that both reads and sets coordinates.

Classes Point and Rectangle for geometry.

Examples:

m2 position: m1 position.
m2 center: World center.
m2 left: m1: right.
m2 extent: 200@200.
m2 bounds: ((0@0) extent: (200@200)).
m2 bounds: World bounds.
More messages:
left (Integer)
right (Integer)
top (Integer)
bottom (Integer)
position (Point)
center (Point)
topLeft (Point)
bottomRight (Point)
extent (Point)
bounds (Rectangle)

Drawing

Subclass and override drawOn:

Example:

drawOn: aCanvas
    aCanvas fillOval: self bounds color: self color.
This draws "off screen", Squeak handles double-buffring.

The message changed can be sent to a morph to cause it to redisplay. Many methods in class Morph calls changed, there is seldom a need to use this method.

The display can be drawn on directly using the global variable Display, which is an instance of class Form.

Form is the basic class for images.

BitBlt and WarpBlt are classes for copying images. (More about this in a forthcoming lecture).

Mouse event

Two techniques:

Examples of how to use mouse methods

Mouse down/up methods:

handlesMouseDown: event
    ^true
    
mouseDown: event
    event 
    self left: self left + 10.
    
mouseUp: event
    self left: self left - 10.
Mouse drag metods:
handlesMouseOver: event
    ^ true
    
mouseMove: event 
    self center: event cursorPoint.
    
mouseEnter: anEvent
    self color: Color red.

mouseLeave: anEvent
    self color: Color blue.

Examples of how to use event listeners

Use this style to register mouse events:
halo
    on: #mouseDown
    send: #delete
    to: self.
        
halo
   on: #mouseDown
   send: #openPopupMenu
   to: self.
delete is an existing methods, openPopupMenu needs to be implemented.

Popup menus

This is an example of a "World-menu" for a drawing area.
openPopupMenu
    | menu |
    menu := MenuMorph new.
    menu addTitle: 'Main menu'.
    menu add: 'New Example Morph' target: self action: #createExampleMorph.
    menu add: 'New Watch Morph' target: self action: #createWatchMorph.
    menu addLine.
    menu add: 'Undo' target: self action: #undoLastCommand.
    menu addLine.
    menu add: 'Clear' target: self action: #clearDrawingArea.
    menu popUpInWorld.
    
createExampleMorph
    | m |
    m := ExampleMorph new.
    m center: World primaryHand position.
    World primaryHand addMorph: m.
    
createWatchMorph
    | m |
    m := WatchMorph new.
    m center: World primaryHand position.
    World primaryHand addMorph: m.
    
undoLastCommand
    ...
    
clearDrawingArea
    self sumorphs do: [:m | m delete].
    "There is a method for this in class Morph: removeAllMorphs"

Buttons

button := SimpleButtonMorph new.
button borderWidth: 2.
button extent: 100@35.
button label: 'Delete me'.
button target: button.
button actionSelector: #delete.

Custom halos (handles)

Create halos and add to self in mouseEnter.

Delete halos (e.g. use removeAllMorphs) in mouseLeave.

Code the desired behaviour in the halos, e.g. as "listener" methods in the main morph (the morph having the halos).

Code example for a morph that has two halos

Morph subclass: #DemoMorph
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ETE257-Demo'

initialize
       super initialize.
    self setAppearence.
    self createMouseOverHandlers.
    
setAppearence
       self color: Color orange.
       self extent: 200@200.
       self borderWidth: 2.
       self borderColor: Color black.
       
createMouseOverHandlers
    self on: #mouseEnter send: #createHalos to: self.
    self on: #mouseLeave send: #deleteHalos to: self.

createHalos
    | deleteHalo menuHalo |
    
    deleteHalo := EllipseMorph new.
    menuHalo := EllipseMorph new.
    
    deleteHalo
        color: Color red;
        borderWidth: 1;
        borderColor: Color black;
        extent: 20@20.
        
    menuHalo
        color: Color blue;
        borderWidth: 1;
        borderColor: Color black;
        extent: 20@20.
    
    deleteHalo on: #mouseDown send: #deleteMe to: self.
    menuHalo on: #mouseDown send: #popupMenu to: self.
    
    deleteHalo topLeft: self topLeft + 5.
    menuHalo topLeft: deleteHalo topRight + (10@0).
    
    self addMorph: deleteHalo.
    self addMorph: menuHalo.

deleteHalos
    self submorphs do: [:m | m delete].


drawOn: aCanvas
    "Draw the morph here."
    
popupMenu
    "Popup a menu here"
    
deleteMe
    self delete.    

Code example for a resize halo

EllipseMorph subclass: #ResizeHalo
    instanceVariableNames: 'offset'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ETE257-Demo'
    
initialize
    super initialize.
    self
        color: Color yellow;
        borderWidth: 1;
        borderColor: Color black;
        extent: 20@20.
        
handlesMouseDown: event
    ^true
    
mouseDown: event
    offset := event cursorPoint - self owner bottomRight.
    
mouseMove: event
    self owner extent: (event cursorPoint - self owner topLeft - offset).
    self bottomRight: self owner bottomRight - 3.

Command objects and Undo

It can be more flexible to use separate command object instead of puting the command code into the UI-object.

When using this technique (the Command design pattern) one uses one object for each command.

A command object has two methdods, do and undo, and stores the information needed to undo the command.

Example of a command that deletes a morph:

Object subclass: #DeleteCommand
    instanceVariableNames: 'morph parent'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ETE257-Test'
    
do
    morph delete.
    "The following is an example of how
    the command object is added to a list
    of commands kept by an undo manager.
    Note that you have to write the UndoManger
    class yourself."
    UndoManager addCommand: self.
    
morph: aMorph
    morph := aMorph.
    parent := aMorph owner.

undo
    parent addMorph: morph.
Example of how to use the command object in a menu:
command := DeleteCommand new.
command morph: self.
menu add: 'Delete' target: command action: #do.
To undo the command, send the undo message to it.

Typically, command objects are kept in a list, and and the undo mechanism uses this list when undoing commands.

There is some built-in support in Morphic for command objects and undo in the classes Command and Command History. (Note that the above examples does not use this mechanism.)

Layouts

Manual layout, override layoutChanged:
layoutChanged
    super layoutChanged. "I sthis needed?"
    "Position submorphs here"
Table layout, set up the layout in for example initialize:
initialize
    super initialize.
    
    self 
        color: Color white;
        borderWidth: 0;
        changeTableLayout;
        listDirection: #leftToRight;
        cellPositioning: #topLeft.
            
    menuPanel := Morph new.
    menuPanel
        color: Color orange muchLigther;
        borderWidth: 0;
        hResizing: #shrinkWrap; 
        vResizing: #spaceFill;
        changeTableLayout;
        listDirection: #topToBottom;
        cellPositioning: #topLeft;
        cellInset: 10@10;
        layoutInset: 5@5.
    
    "ToDo: Add some objects (e.g. buttons) to the menu."
    
    drawingArea := Morph new.
    drawingArea
        color: Color white;
        borderWidth: 0.
        
    self addMorphBack: menuPanel.
    self addMorphBack: drawingArea.
    
    self extent: 600@500.
The symbol #rigid can also be used, they you set the width or height yourself.

You can also create a table layout with:
self layoutPolicy: TableLayout new.
(Instead of using self changeTableLayout.)

Proportional layout:

initialize
    | frame |
    super initialize.
    
    self layoutPolicy: ProportionalLayout new.
    
    frame _ LayoutFrame 
        fractions: (0@0 corner: 0.0@1.0) 
        offsets: (0@0 corner: 100@0).
    self addMorph: menuPanel fullFrame: frame.
    
    frame _ LayoutFrame 
        fractions: (0@0 corner: 1.0@1.0) 
        offsets: (100@0 corner: 0@0).
    self addMorph: drawingArea fullFrame: frame.
Write your own layout manager:
LayoutPolicy subclass: #MyLayout

layout: aMorph in: newBounds
    aMorph doLayout.
In the above example the morphs handles its own layout in the method doLayout (you have to write this method yourself). This is a more stable approach than overriding layoutChanged.

SystemWindow

Examples of how to open a morph in a system window.
WatchMorph new openInWindow.

WatchMorph new openInWindowLabeled: 'Clock'.

window := WatchMorph new openInWindowLabeled: 'Clock'.
window color: Color green.

Cursor (Hand)

To put something in the hand:
mouseDown: event
    event hand grabMorph: self.
    "Or: event hand addMorph: self"
You can use World primaryHand if you do not have an event object.

Drag & Drop Support

Class PasteUpMorph supports drag and drop automatically.

Call enableDragNDrop on your own morph to allow drag&drop.

The following are some of the methods you can override to control drag/drop actions:

aboutToBeGrabbedBy: aHand
justDroppedInto: aMorph event: anEvent
justGrabbedFrom: formerOwner
wantsDroppedMorph: aMorph event: evt
wantsToBeDroppedInto: aMorph

FillInTheBlankMorph

This class provides a simple way to read a string from the user. Here is an example:
answer := FillInTheBlankMorph
    request: 'What is your name?'
    initialAnswer: ''.
    
answer ifNotEmpty: [
    PopUpMenu inform: 'Welcome ', answer, '!'.
    ].

Forms

Class Form is the basic class for images in Smalltalk. It is not an UI-object (not a morph). Display is a global variable that refers to the display form.

Examples:

"Open a picture file."
f := Form fromFileNamed: 'Blobb.gif'.

"It is usually good to use 32 bit forms."
f := f asFormOfDepth: 32.

"Scale a form."
f := f scaledIntoFormOfSize: 50@50.

"Draw a form on a canvas (with transparency)."
canvas paintImage: form at: 100@100.

"Draw a form directly on the display."
f displayOn: Display at: 100@100.

"Draw with a combination rule."
f displayOn: Display at: 200@300 rule: Form reverse.

"Save part of the display as an PNG image."
form := Form fromUser.
writer := PNGReadWriter on: (FileStream forceNewFileNamed: 'MyPicture.png') binary.
writer nextPutImage: form.
writer close.

"Save a morph as a GIF image"
clock := WatchMorph new.
GIFReadWriter putForm: clock imageForm onFileNamed: 'Clock.gif'.

BitBlt and WarpBlt

These are low-level pixel transfering classes. BitBlt = Bit Block Transfer. WarpBlt adds rotating and skewing capabilities. See examples on the class-side of WarpBlt. A combination rule is used to specify how pixels in the source and the target should be combined.

Simple example of using WarpBlt to scale a form:

f := Form fromUser.
warp := (WarpBlt toForm: Display)
    cellSize: 1;
    sourceForm: f;
    cellSize: 2;  "installs a colormap"
    combinationRule: Form over.
r := (0@0) extent: f extent.
pts := {r topLeft. r bottomLeft. r bottomRight. r topRight}.
warp 
    copyQuad: pts 
    toRect: ((0@0) extent: (200@200)).
Note that it is usually easier to use high-level methods in Form to manipulate images, and to use drawing methods in Canvas and FormCanvas to draw images in more fancy ways, than to use BitBlt and WarpBlt.

Streams and serializing

ReferenceStream is a way of serializing a tree of objects into disk file. A ReferenceStream can store one or more objects in a persistent form, including sharing and cycles.

Here is the way to use ReferenceStream:

rr := ReferenceStream fileNamed: 'test.obj'.
rr nextPut: obj.
rr close.
To get it back:
rr := ReferenceStream fileNamed: 'test.obj'.
obj := rr next.
rr close.
You can also use SmartRefStream, which can handle changes in the code for the objects.

StandardFileMenu

StandardFileMenu is used as a simple file open dialog.
result := 
    (
    StandardFileMenu 
        oldFileMenu: (FileDirectory default)
        withPattern: '*.*'
    )
    startUpWithCaption: 'Select a picture file'.
    
result ifNotNil: [
    picture := Form fromBinaryStream: 
                    (result directory readOnlyFileNamed: result name) binary.
    sketch := SketchMorph new.
    sketch form: picture.
    sketch openInHand.
    ].
SketchMorph is a morph that can be used to display forms. (You can also make your own morph class for this purpose.)

Inform

PopUpMenu inform: 'System is going down!', String cr, 'Take cover!'.