Appendix F Art of Morph package

Download Art of Morph package

'From Cuis7.5 [latest update: #7387] on 6 August 2025 at 9:39:50 pm'!
'Description '!
!provides: 'ArtOfMorph' 1 14!
!requires: 'UI-Widgets' 1 54 nil!
!requires: 'UI-Panel' 1 130 nil!
SystemOrganization addCategory: #ArtOfMorph!


!classDefinition: #MedicCross category: #ArtOfMorph!
PlacedMorph subclass: #MedicCross
   instanceVariableNames: 'width'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'ArtOfMorph'!
!classDefinition: 'MedicCross class' category: #ArtOfMorph!
MedicCross class
   instanceVariableNames: ''!

!classDefinition: #Ruler category: #ArtOfMorph!
PlacedMorph subclass: #Ruler
   instanceVariableNames: 'length lastHandPosition font'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'ArtOfMorph'!
!classDefinition: 'Ruler class' category: #ArtOfMorph!
Ruler class
   instanceVariableNames: ''!

!classDefinition: #EllipseDemo category: #ArtOfMorph!
ColoredBoxMorph subclass: #EllipseDemo
   instanceVariableNames: 'shrink'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'ArtOfMorph'!
!classDefinition: 'EllipseDemo class' category: #ArtOfMorph!
EllipseDemo class
   instanceVariableNames: ''!

!classDefinition: #FileRequestMorph category: #ArtOfMorph!
StringRequestMorph subclass: #FileRequestMorph
   instanceVariableNames: 'fileBrowser'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'ArtOfMorph'!
!classDefinition: 'FileRequestMorph class' category: #ArtOfMorph!
FileRequestMorph class
   instanceVariableNames: ''!

!classDefinition: #FileSelectorPane category: #ArtOfMorph!
FlowLayoutMorph subclass: #FileSelectorPane
   instanceVariableNames: ''
   classVariableNames: ''
   poolDictionaries: ''
   category: 'ArtOfMorph'!
!classDefinition: 'FileSelectorPane class' category: #ArtOfMorph!
FileSelectorPane class
   instanceVariableNames: ''!


!MedicCross commentStamp: 'hlsf 7/29/2025 15:53:25' prior: 0!
A flashing medical cross

   MedicCross new openInHand

!

!MedicCross methodsFor: 'initialization' stamp: 'hlsf 7/29/2025 15:52:21'!
initialize
   super initialize.
   width := 0! !

!MedicCross methodsFor: 'drawing' stamp: 'hlsf 7/29/2025 15:53:09'!
drawOn: aCanvas
   aCanvas strokeWidth: width color: Color green lighter fillColor: Color green do: [
      aCanvas polyLine: {
         100@0 .   140@0 .   140@100 . 240@100 . 240@140 .140@140 .
         140@240 . 100@240 . 100@140 . 0@140 .    0@100 . 100@100.
         100@0 } ].! !

!MedicCross methodsFor: 'stepping' stamp: 'hlsf 7/29/2025 22:42:13'!
step
   width := width + 0.2.
   width > 30 ifTrue: [width := 0].
   self redrawNeeded.! !

!MedicCross methodsFor: 'stepping' stamp: 'hlsf 7/29/2025 15:46:34'!
stepTime
   ^ 10 ! !

!MedicCross methodsFor: 'stepping' stamp: 'hlsf 7/29/2025 15:52:40'!
wantsSteps
   ^ true ! !

!Ruler methodsFor: 'drawing' stamp: 'hlsf 7/31/2025 23:59:02'!
drawOn: canvas
   | grad posX extent step roundedLength |
   roundedLength := length rounded.
   extent := roundedLength * self ppcm + (self ppcm / 2) @ 60.
   
   canvas fillRectangle: (-5@0 corner: extent x @ 25) color: Color yellow.
   canvas fillRectangle: (-5@25 corner: extent ) color: (Color yellow alpha: 0.5).
   canvas frameRectangle: (-5@0 corner: extent) borderWidth: 0.5 color: Color yellow muchDarker .

   step :=  self ppcm / 2. " half centimeter step "
   canvas strokeWidth: 0.8 color: Color black do: [
      posX := 0.
      roundedLength + 1 timesRepeat: [
         canvas moveTo: posX @ 0.5; lineToY: 10.
         canvas moveTo: (posX  + step) @ 0.5 ; lineToY: 6.
         posX := posX + self ppcm] ].

   step := self ppcm / 10. "millimeter step "
   canvas strokeWidth: 0.3 color: Color black do: [
      posX := step.
      roundedLength * 2 + 1 timesRepeat: [
         4 timesRepeat: [
            canvas moveTo: posX @ 0.2; lineToY: 4.
            posX := posX + step].
         posX := posX + step] ].

   grad := posX := 0.
   roundedLength + 1 timesRepeat: [
      canvas drawString: grad asString atCenterX: posX @12 font: font color: Color black.
      grad := grad + 1.
      posX := posX + self ppcm].! !

!Ruler methodsFor: 'accessing' stamp: 'hlsf 7/31/2025 23:53:55'!
length: newLength
   length := newLength max: 1.
   self positioningButtons.
   self redrawNeeded ! !

!Ruler methodsFor: 'accessing' stamp: 'hlsf 8/5/2025 11:05:48'!
ppcm
" pixels per cm "
   ^ 50.0! !

!Ruler methodsFor: 'initialization' stamp: 'hlsf 7/31/2025 23:57:21'!
initialize
   super initialize.
   font := FontFamily familyName: FontFamily defaultFamilyName pointSize: 8.
   length := 10 "cm".
   self insertButtons ! !

!Ruler methodsFor: 'initialization' stamp: 'hlsf 7/31/2025 20:28:28'!
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 
   ! !

!Ruler methodsFor: 'initialization' stamp: 'hlsf 7/31/2025 20:28:28'!
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 ]
      ! !

!Ruler methodsFor: 'geometry' stamp: 'hlsf 7/31/2025 14:36:31'!
rotationCenter
   ^ `0@0`! !

!Ruler methodsFor: 'as yet unclassified' stamp: 'hlsf 8/1/2025 00:04:38'!
resizeRuler
   | event prev |
   "any thing 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].! !

!Ruler methodsFor: 'as yet unclassified' stamp: 'hlsf 8/1/2025 10:35:11'!
rotateRuler
   | event p1 v1 v2|
   "any thing 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].! !

!EllipseDemo methodsFor: 'accessing' stamp: 'hlsf 6/14/2025 12:17:51'!
center
   ^  extent / 2.0! !

!EllipseDemo methodsFor: 'accessing' stamp: 'hlsf 8/6/2025 21:39:03'!
semiAxes
" the semi minor and major axis of the ellipse"
   ^  (extent / 2.0) - shrink ! !

!EllipseDemo methodsFor: 'initialization' stamp: 'hlsf 6/11/2025 18:54:04'!
defaultExtent
   ^ 200@200! !

!EllipseDemo methodsFor: 'initialization' stamp: 'hlsf 6/19/2025 23:45:40'!
initialize
   super initialize.
   color := Color yellow.
   shrink := 0.
   'Hover over the circle to change its color and unhover to change it back.' print.
   'Click it with left or right button to shrink or to grow the ellipse.' print.
   'Move mouse over the circle and press r, g, or b to change its color.' print.! !

!EllipseDemo methodsFor: 'drawing' stamp: 'hlsf 8/6/2025 21:39:03'!
drawOn: aCanvas
   aCanvas fillColor: color do: [
       aCanvas ellipseCenter: self center radius: self semiAxes ]! !

!EllipseDemo methodsFor: 'event handling testing' stamp: 'hlsf 6/19/2025 23:48:44'!
handlesKeyboard
"This enables the morph to handle key events if it has focus."
    ^ self visible! !

!EllipseDemo methodsFor: 'event handling testing' stamp: 'hlsf 6/14/2025 13:31:52'!
handlesMouseDown: aMouseEvent
"This enables the morph to handle mouse events such as button presses."
    ^ true! !

!EllipseDemo methodsFor: 'event handling testing' stamp: 'hlsf 6/14/2025 12:53:26'!
handlesMouseOver: aMouseEvent
"This enables the morph to handle mouse enter and leave events."
    ^ true! !

!EllipseDemo methodsFor: 'events' stamp: 'hlsf 6/29/2025 12:10:49'!
keyStroke: aKeyEvent
   | character increment h s v |
   super keyStroke: aKeyEvent.
   aKeyEvent wasHandled ifTrue: [^ self].
   character := Character codePoint: aKeyEvent keyValue.
   color := character 
      caseOf: {
         [ $r ] -> [ `Color red` ].
         [ $g ] -> [ `Color green` ].
         [ $b ] -> [ `Color blue` ] }
      otherwise: [color].
   
   h := color hue.
   s := color saturation.
   v := color brightness .
   increment := aKeyEvent controlKeyPressed ifTrue: [-0.1] ifFalse: [0.1].
   character 
      caseOf:   {
         [ $h ] -> [ h := h + (increment * 13) ].
         [ $s ] -> [ s := s + increment ].
         [ $v ] -> [ v := v + increment ]
      } 
      otherwise: [].
   color setHue: h saturation: s brightness: v.
   self redrawNeeded! !

!EllipseDemo methodsFor: 'events' stamp: 'hlsf 6/19/2025 23:52:50'!
mouseButton1Down: aMouseEvent localPosition: aPosition
    shrink := (shrink + 0.5) min: (extent x min: extent y) // 2.
    (Preferences at: #focusFollowsMouse) ifFalse: [aMouseEvent hand newKeyboardFocus: self].
    self redrawNeeded! !

!EllipseDemo methodsFor: 'events' stamp: 'hlsf 6/14/2025 13:33:22'!
mouseButton2Down: aMouseEvent localPosition: aPosition
    shrink := (shrink - 5) max: 0.
    self redrawNeeded! !

!EllipseDemo methodsFor: 'events' stamp: 'hlsf 6/19/2025 23:52:35'!
mouseEnter: aMouseEvent
    color := `Color green`.
    "If the user opted for focus to automatically
     move focus to the morph under the cursor then tell
     the cursor (event hand) to give focus to this morph."
    (Preferences at: #focusFollowsMouse) ifTrue: [aMouseEvent hand newKeyboardFocus: self].
    self redrawNeeded.! !

!EllipseDemo methodsFor: 'events' stamp: 'hlsf 6/28/2025 09:36:28'!
mouseLeave: aMouseEvent
   super mouseLeave: aMouseEvent.
   color := `Color red`.
   self redrawNeeded.! !

!EllipseDemo methodsFor: 'events' stamp: 'hlsf 6/14/2025 13:09:40'!
wantsContour 
   ^ true! !

!EllipseDemo methodsFor: 'geometry testing' stamp: 'hlsf 6/11/2025 19:05:04'!
requiresVectorCanvas
   ^ true ! !

!FileRequestMorph methodsFor: 'initialization' stamp: 'hlsf 7/27/2025 14:46:09'!
addTextPane
   super addTextPane.
   submorphs first addMorph: 
         ((PluggableButtonMorph model: self action: #toggleFileBrowser)
            setBalloonText: 'Browse the file system to select a file or directory';
            icon: (Theme current fetch: #( '16x16' 'places' 'folder' )) )! !

!FileRequestMorph methodsFor: 'accessing' stamp: 'hlsf 7/27/2025 15:10:38'!
directoryEntry
   | editedContents | 
   editedContents := textMorph scroller contents asPlainString.
   ^ editedContents asFileEntry exists
      ifTrue: [editedContents asFileEntry parent] 
      ifFalse: [editedContents asDirectoryEntry exists 
         ifTrue: [editedContents asDirectoryEntry ] ifFalse: [editedContents asDirectoryEntry parent] ]! !

!FileRequestMorph methodsFor: 'accessing' stamp: 'hlsf 7/27/2025 14:33:32'!
response: aText
   | answer |
   answer := super response: aText.
   (answer and: [fileBrowser notNil]) ifTrue: [fileBrowser delete].   
   ^ answer! !

!FileRequestMorph methodsFor: 'accessing' stamp: 'hlsf 7/27/2025 12:59:21'!
toggleFileBrowser
   fileBrowser ifNotNil: [
      fileBrowser delete.
      fileBrowser := nil.
      ^ self].
   fileBrowser := FileSelectorPane new   open: self directoryEntry.
   fileBrowser when: #selectedFile send: #entry: to: self.
   fileBrowser 
      color: (Color white alpha: 0.8);
      morphPosition: self morphPosition + (0 @ (self morphHeight + 5));
      morphExtent: self morphExtent * (1@5);
      openInWorld

   ! !

!FileRequestMorph methodsFor: 'events' stamp: 'hlsf 7/29/2025 10:48:13'!
entry: anEntry
   textMorph hasUnacceptedEdits: false.
   response := anEntry asString.
   self changed: #response.
   textMorph hasUnacceptedEdits: true.! !

!FileRequestMorph methodsFor: 'private' stamp: 'hlsf 7/27/2025 12:50:16'!
cancel
   super cancel.
   fileBrowser ifNotNil: [fileBrowser delete]! !

!FileRequestMorph methodsFor: 'private' stamp: 'jmv 7/28/2025 14:40:16'!
ok
   self delete.
   fileBrowser ifNotNil: [fileBrowser delete]! !

!FileRequestMorph class methodsFor: 'as yet unclassified' stamp: 'hlsf 7/27/2025 18:09:49'!
example
"
   FileRequestMorph example   
"
   ^ FileRequestMorph 
      request: 'Select a file or directory' 
      initialAnswer: DirectoryEntry userBaseDirectory asString 
      orCancel: nil.
! !

!FileRequestMorph class methodsFor: 'instance creation' stamp: 'hlsf 7/29/2025 10:54:20'!
request: queryString
   ^ self request: queryString initialAnswer: DirectoryEntry userBaseDirectory asString! !

!FileSelectorPane methodsFor: 'initialization' stamp: 'hlsf 7/19/2025 13:26:16'!
initialize
   super initialize.
   self open: DirectoryEntry userBaseDirectory
   ! !

!FileSelectorPane methodsFor: 'private' stamp: 'hlsf 7/28/2025 23:53:42'!
entryPreviewFor: fileEntry
   | fileView |
   fileView := FilePreviewMorph 
      object: fileEntry 
      image: ((fileEntry isFileEntry 
         ifTrue: [Theme current genericTextIcon] 
         ifFalse: [Theme current fetch: #( '16x16' 'places' 'folder' )]) magnifyTo: 64@64)
      buttons: nil
      label: (fileEntry isRoot ifTrue:['/'] ifFalse: [fileEntry baseName]) ::
      borderColor: Color transparent;
      color: Color transparent.
   fileEntry isDirectoryEntry ifTrue: [
      fileView when: #doubleClick send: #open: to: self with: fileEntry].
   fileView when: #selected send: #toggleSelection: to: self with: fileView.
   ^ fileView ! !

!FileSelectorPane methodsFor: 'private' stamp: 'hlsf 7/19/2025 11:57:59'!
previewsFor: entries
   ^ (entries sort: [:a :b | a baseName asUppercase < b baseName asUppercase ]) collect: [:anEntry | 
      self entryPreviewFor: anEntry]! !

!FileSelectorPane methodsFor: 'accessing' stamp: 'hlsf 7/29/2025 10:48:53'!
open: aDirectoryEntry
   | entryViews |
   model := aDirectoryEntry.
   entryViews := OrderedCollection new.
   model isRoot ifFalse: [ | parentView |
      parentView := self entryPreviewFor: model parent.
      parentView relabel: '..' bold.
      entryViews add: parentView].
   entryViews 
      addAll: (self previewsFor: model directories);
      addAll: (self previewsFor: model files ).
   self cells: entryViews.
   self updateLayout ! !

!FileSelectorPane methodsFor: 'events' stamp: 'hlsf 7/25/2025 19:22:15'!
toggleSelection: fileView
   | selectedView |
   selectedView := cells detect: [:aFileView | aFileView isSelected] ifNone: [nil].   
   selectedView = fileView
      ifTrue: [
         fileView toggleSelection. " unselect view, no view selected anymore "
         selectedView := nil]         
      ifFalse: [
         selectedView ifNotNil: [selectedView toggleSelection].
         fileView selected: true.
         selectedView := fileView].
   selectedView 
      ifNil: [self triggerEvent: #noSelection]
      ifNotNil: [self triggerEvent: #selectedFile with: selectedView fileEntry]! !