ShowMeHowItWorks.st
author Claus Gittinger <cg@exept.de>
Fri, 19 Jul 2019 15:18:55 +0200
changeset 3709 6189f7800242
parent 3700 34af8cf33242
child 3711 a472042fd298
permissions -rw-r--r--
#UI_ENHANCEMENT by cg class: MethodFinderWindow changed: #showMeHowItWorks

"{ Package: 'stx:libtool2' }"

"{ NameSpace: Smalltalk }"

Object subclass:#ShowMeHowItWorks
	instanceVariableNames:'opStream lastComponentName lastComponent'
	classVariableNames:''
	poolDictionaries:''
	category:'Interface-Help'
!

!ShowMeHowItWorks class methodsFor:'documentation'!

documentation
"
    documentation to be added.

    class:
        <a short class summary here, describing what instances represent>

    responsibilities:    
        <describing what my main role is>

    collaborators:    
        <describing with whom and how I talk to>

    API:
        <public api and main messages>
        
    example:
        <a one-line examples on how to use - can also be in a separate example method>

    implementation:
        <implementation points>

    [author:]
        Claus Gittinger

    [instance variables:]

    [class variables:]

    [see also:]

"
!

example
    ShowMeHowItWorks do:#(
        ( showing: 'Choose the number of arguments' do:(
            moveTo: NumberOfArguments
            select: '1' 
        ))  
        (showing: 'Click into the "receiver" field' do:(
            moveTo: ReceiverEditor
            click: ReceiverEditor 
        ))
        (showing: 'Enter a value (or expression) into "receiver" field' do:(
            enter: '100'
        ))
        (showing: 'Click into the "first argument" field' do:(
            moveTo: Arg1Editor
            click: ReceiverEditor
        ))
        (showing: 'Enter a value (or expression) into "receiver" field' do:(
            enter: '100'
        ))

    )
! !

!ShowMeHowItWorks class methodsFor:'running'!

do:specArray
    self doStream:specArray readStream

    "Created: / 19-07-2019 / 10:52:59 / Claus Gittinger"
    "Modified: / 19-07-2019 / 14:30:43 / Claus Gittinger"
!

doStream:specStream
    "must run as a separate process;
     otherwise - if started by the app itself -
     no events will be processed while running"
     
    [
        self new doStream:specStream
    ] fork.

    "Created: / 19-07-2019 / 10:53:07 / Claus Gittinger"
    "Modified (comment): / 19-07-2019 / 14:30:29 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'commands'!

pause
    <action>
    
    Dialog information:'Show Paused.\Click on "OK" to proceed'

    "Created: / 19-07-2019 / 15:03:17 / Claus Gittinger"
!

showing:message do:operations
    "execute operations while showing (and speaking) some message."

    <action>
    
    |messageView talkDone|

    self assert:operations isSequenceable.

    messageView := ActiveHelpView for:message.
    "/ messageView shapeStyle:#cartoon.
    [
        messageView realize.

        self talking ifTrue:[
            talkDone := Semaphore new.
            [
                self tell:message.
                talkDone signal
            ] fork.
            
            "/
            "/ allow speaker some headoff
            Delay waitForSeconds:(message size / 20).
        ].

        self doStream:(operations readStream).
    ] ensure:[
        messageView destroy
    ].
    self talking ifTrue:[
        talkDone wait.
    ].

    "Created: / 19-07-2019 / 11:19:27 / Claus Gittinger"
    "Modified (format): / 19-07-2019 / 15:02:12 / Claus Gittinger"
!

wait:seconds
    <action>
    
    Delay waitForSeconds:seconds

    "Created: / 19-07-2019 / 15:09:45 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'commands - mouse'!

click:buttonNr
    "press-release"
    
    <action>

    self click:buttonNr inComponent:lastComponent

    "Created: / 19-07-2019 / 13:21:20 / Claus Gittinger"
    "Modified: / 19-07-2019 / 14:55:18 / Claus Gittinger"
!

enter:aString
    "enter text into the last component"

    <action>

    lastComponent simulateTextInput:aString at:(lastComponent extent // 2) sendDisplayEvent:false

    "Created: / 19-07-2019 / 14:29:27 / Claus Gittinger"
!

moveTo:componentName
    "move the mouse to componentName,
     then circle around it a few times"

    <action>

    |component|

    lastComponentName := componentName.
    
    component := self findComponent:componentName.
    component isNil ifTrue:[
        self error:'no component found for: ',componentName.
    ].
    lastComponent := component.

    self movePointerToComponent:component.
    self circlePointerAroundComponent:component.

    "Created: / 19-07-2019 / 11:20:42 / Claus Gittinger"
    "Modified (format): / 19-07-2019 / 14:55:27 / Claus Gittinger"
!

select:itemsLabel
    "select an item by label,
     allowed after moving to:
        aComboBox
        aSelectionInListView
    "    

    <action>

    |idx|
    
    (lastComponent isKindOf:ComboView) ifTrue:[
        "/ click on the menubutton
        self movePointerToComponent:lastComponent menuButton.
        self click:1 inComponent:lastComponent menuButton.
        Delay waitForSeconds:0.3.
        (idx := lastComponent list indexOf:itemsLabel ifAbsent:[nil]) isNil ifTrue:[
            self error:'no such item in comboList: ',itemsLabel
        ].
        lastComponent select:idx.
        Delay waitForSeconds:0.3.
        lastComponent shownMenu notNil ifTrue:[
            lastComponent shownMenu hide.
        ].    
        ^ self
    ].    
    self error:'cannot select this component'

    "Created: / 19-07-2019 / 12:34:25 / Claus Gittinger"
    "Modified (format): / 19-07-2019 / 14:55:34 / Claus Gittinger"
!

selectIndex:itemsIndex
    "select an item by index,
     allowed after moving to:
        aComboBox
        aSelectionInListView
    "    

    <action>

    (lastComponent isKindOf:ComboView) ifTrue:[
        "/ click on the menubutton
        self movePointerToComponent:lastComponent menuButton.
        self click:1 inComponent:lastComponent menuButton.
        Delay waitForSeconds:0.5.
        lastComponent select:itemsIndex.
        Delay waitForSeconds:0.5.
self halt.
        ^ self
    ].    
    self error:'cannot select this component'

    "Created: / 19-07-2019 / 14:20:11 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'defaults'!

circlingCount
    "circle around move-end position that many times"

    ^ 3

    "Created: / 19-07-2019 / 13:03:45 / Claus Gittinger"
!

circlingRadius 
    "radius when circling"
    
    ^ 30 "/ pixels

    "Created: / 19-07-2019 / 13:07:59 / Claus Gittinger"
!

circlingSpeed 
    "time per round when circling"
    
    ^ 0.3 seconds.       "/ time per round

    "Created: / 19-07-2019 / 13:02:34 / Claus Gittinger"
!

clickTime
    "when clicking"

    ^ 100 milliseconds

    "Created: / 19-07-2019 / 13:17:20 / Claus Gittinger"
!

pointerAnimationDelay
    ^ 50 milliseconds.   "/ 20 updates per second

    "Created: / 19-07-2019 / 13:04:45 / Claus Gittinger"
!

pointerMoveSpeed
    ^ 400.   "/ pixels per second

    "Created: / 19-07-2019 / 13:05:40 / Claus Gittinger"
!

talking
    ^ true

    "Created: / 19-07-2019 / 14:31:14 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'helper'!

findComponent:componentName
    "find a component by name - in the active and possibly in any app"
    
    |app component candidates|

    app := WindowGroup activeMainApplication.
    app notNil ifTrue:[ 
        component := self findComponent:componentName in:app.
    ].
    
    component isNil ifTrue:[
        "/ search through all current applications
        candidates := OrderedCollection new.
        WindowGroup scheduledWindowGroups do:[:eachWG |
            |eachApp|

            (eachApp := eachWG application) notNil ifTrue:[
                component := self findComponent:componentName in:eachApp.
                component notNil ifTrue:[ 
                    candidates add:component
                ].
            ].
        ].
        
        candidates size == 1 ifTrue:[
            component := candidates first
        ] ifFalse:[    
            candidates notEmpty ifTrue:[
                self error:'multiple components found by name: ',componentName.
            ]    
        ].    
    ].    
    ^ component

    "Created: / 19-07-2019 / 12:02:30 / Claus Gittinger"
!

findComponent:componentName in:anApplication
    |component componentNameSymbol foundByName foundByTitle foundByLabel|

    (component := anApplication componentAt:componentName) notNil ifTrue:[^ component].
    (componentNameSymbol := componentName asSymbolIfInterned) notNil ifTrue:[
        (component := anApplication componentAt:componentNameSymbol) notNil ifTrue:[^ component].
    ].
    
    "/ mhmh - search through all widgets of anApplication; 
    "/ maybe it was not created via the builder/spec,
    "/ or it has changed its name.
    "/ look for: widget's name, widget's title, widget's label
    foundByName := OrderedCollection new. 
    foundByTitle := OrderedCollection new. 
    foundByLabel := OrderedCollection new.
    
    anApplication window allSubViewsDo:[:each |
        [
            each name = componentName ifTrue:[ foundByName add:each ].
        ] on:MessageNotUnderstood do:[:ex | ].
        [
            each title = componentName ifTrue:[ foundByTitle add:each ].
        ] on:MessageNotUnderstood do:[:ex | ].
        [
            each label = componentName ifTrue:[ foundByLabel add:each ].
        ] on:MessageNotUnderstood do:[:ex | ].
    ].
    foundByName notEmpty ifTrue:[
        self assert:(foundByName size == 1) message:'multiple components found by name'.
        ^ foundByName first.
    ].
    foundByTitle notEmpty ifTrue:[
        self assert:(foundByTitle size == 1) message:'multiple components found by title'.
        ^ foundByTitle first.
    ].
    foundByLabel notEmpty ifTrue:[
        self assert:(foundByLabel size == 1) message:'multiple components found by label'.
        ^ foundByLabel first.
    ].
    ^ component

    "Created: / 19-07-2019 / 11:36:21 / Claus Gittinger"
!

tell:message
    self talking ifTrue:[
        OperatingSystem speak:message.
    ].

    "Created: / 19-07-2019 / 14:57:50 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'helpers - broken'!

click:buttonNr atPosition:position
    "press-release at position"
    
    |screen|

    screen := Screen current.
    
    screen setPointerPosition:position.    
    screen flush.
    self click:buttonNr

    "Created: / 19-07-2019 / 13:14:51 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'helpers - mouse movement'!

circlePointerAroundComponent:component
    "circle around it a few times"
    
    self circlePointerAroundPosition:(component screenBounds center rounded)

    "Created: / 19-07-2019 / 13:12:35 / Claus Gittinger"
!

circlePointerAroundPosition:position
    "circle around it a few times"
    
    |screen stepDelayTime numCircles circlingSpeed radius|

    screen := Screen current.
    
    circlingSpeed := self circlingSpeed.    "/ time per round
    numCircles := self circlingCount.
    stepDelayTime := self pointerAnimationDelay.   "/ update interval
    
    radius := self circlingRadius.

    "/ move it around a few times
    1 to:numCircles do:[:round |
        |n angle|

        n := circlingSpeed / stepDelayTime. "/ nr of steps per circle
        angle := 360 / n.                   "/ angle-delta per step
        1 to:n do:[:step |
            |a x y|
            
            a := angle * step.
            "/ clockwise starting above the center
            x := position x + (radius * a degreesToRadians sin).
            y := position y + (radius * a degreesToRadians cos).
"/ Transcript showCR:(x@y).
            screen setPointerPosition:(x@y) rounded.
            screen flush.
            Delay waitFor:stepDelayTime.
        ].    
        "/ and back
        screen setPointerPosition:position rounded.
        screen flush.
        Delay waitFor:stepDelayTime.
    ].

    "Created: / 19-07-2019 / 13:12:40 / Claus Gittinger"
!

click:buttonNr inComponent:component
    "press-release in a component"

    component simulateButtonPress:buttonNr at:(component extent // 2) sendDisplayEvent:false.
    Delay waitForSeconds:(self clickTime).
    component simulateButtonRelease:buttonNr at:(component extent // 2) sendDisplayEvent:false.

"/    self click:buttonNr atPosition:(component extent // 2)

    "Created: / 19-07-2019 / 13:18:27 / Claus Gittinger"
!

movePointerToComponent:aWidget
    "move the mouse to aWidget's center"
    
    self movePointerToPosition:(aWidget screenBounds center rounded).

    "Created: / 19-07-2019 / 13:11:33 / Claus Gittinger"
!

movePointerToPosition:newPosition
    "move the mouse to newPosition"
    
    |screen distance start numSteps moveTime stepDelayTime delta|

    screen := Screen current.
    start := screen pointerPosition.   

    distance := start dist:newPosition.
    moveTime := (distance / self pointerMoveSpeed) seconds. "/ time to move
    stepDelayTime := self pointerAnimationDelay.            "/ update every 50ms
    
    numSteps := moveTime / stepDelayTime.
    numSteps = 0 ifTrue:[
        "/ already there
        ^ self
    ].
    
    delta := (newPosition - start) / numSteps.
    1 to:numSteps do:[:step |
        |p|
        
        p := (start + (delta * step)) rounded.
"/ Transcript showCR:p.
        screen setPointerPosition:p.
        screen flush.
        Delay waitFor:stepDelayTime.
    ].

    "Created: / 19-07-2019 / 12:57:30 / Claus Gittinger"
!

press:buttonNr
    "press at the current position"
    
    |position screen x y|

    screen := Screen current.
    position := screen pointerPosition.
    x := position x.
    y := position y.
    
    self movePointerToPosition:position.

    false "OperatingSystem isOSXlike" ifTrue:[
        |osxPos|

        osxPos := OperatingSystem getMousePosition.
        x := osxPos x rounded.
        y := osxPos y rounded.
        OperatingSystem generateButtonEvent:buttonNr down:true x:x y:y.
        ^ self.
    ].

    screen sendKeyOrButtonEvent:#buttonPress x:x y:y keyOrButton:buttonNr state:0 toViewId:(screen rootWindowId).
    screen flush.

    "Created: / 19-07-2019 / 13:52:38 / Claus Gittinger"
!

release:buttonNr
    "press-release at the current position"
    
    |position screen x y|

    screen := Screen current.
    position := screen pointerPosition.
    x := position x.
    y := position y.
    
    self movePointerToPosition:position.

    false "OperatingSystem isOSXlike" ifTrue:[
        |osxPos|

        osxPos := OperatingSystem getMousePosition.
        x := osxPos x rounded.
        y := osxPos y rounded.
        OperatingSystem generateButtonEvent:buttonNr down:false x:x y:y.
        ^ self.
    ].

    screen sendKeyOrButtonEvent:#buttonRelease x:x y:y keyOrButton:buttonNr state:0 toViewId:(screen rootWindowId).
    screen flush.

    "Created: / 19-07-2019 / 13:53:05 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'running'!

doStream:specStream
    |previousStream|

    previousStream := opStream.
    [
        opStream := specStream.
        [opStream atEnd] whileFalse:[
            self nextCommand.
            Display shiftDown ifTrue:[
                self tell:'Shou stopped by shiftkee'.
                ^ AbortOperationRequest raise
            ].    
        ].    
    ] ensure:[
        opStream := previousStream
    ].
    
"<<END
     ShowMeHowItWorks do:#(
        showing: 'Choose the number of arguments'
        do: (
            moveTo: NumberOfArguments
            select: '1'
        )    
        showing: 'Click into the "receiver" field'
        do: (
            moveTo: ReceiverEditor
            click: ReceiverEditor
        )
        showing: 'Enter a value (or expression) into "receiver" field'
        do: (
            enter: '100'
        )
        showing: 'Click into the "first argument" field'
        do: (
            moveTo: Arg1Editor
            click: ReceiverEditor
        )
        showing: 'Enter a value (or expression) into "receiver" field'
        do: (
            enter: '100'
        )
     )
END"

    "Created: / 19-07-2019 / 10:52:24 / Claus Gittinger"
    "Modified: / 19-07-2019 / 15:00:44 / Claus Gittinger"
!

nextCommand
    |op numArgs sel args method|

    op := opStream next.
    op isArray ifTrue:[
        "/ construct a selector from keyword parts at odd indices
        sel := ((op with:(1 to:op size) select:[:el :idx | idx odd]) asStringWith:'') asSymbol.
        "/ construct arg vector from parts at even indices
        args := op with:(1 to:op size) select:[:el :idx | idx even].
    ] ifFalse:[
        sel := op.
        numArgs := sel argumentCount.
        args := opStream next:numArgs.
    ].
    
    (self respondsTo:sel) ifFalse:[
        self error:'bad operation: ',sel
    ].
    method := self class lookupMethodFor:sel.
    (method hasAnnotation:#action) ifFalse:[self halt].
    
    self perform:sel withArguments:args.

"<<END
     ShowMeHowItWorks do:#(
        showing: 'Choose the number of arguments'
        do: (
            moveTo: NumberOfArguments
            select: '1'
        )    
        showing: 'Click into the "receiver" field'
        do: (
            moveTo: ReceiverEditor
            click: ReceiverEditor
        )
        showing: 'Enter a value (or expression) into "receiver" field'
        do: (
            enter: '100'
        )
        showing: 'Click into the "first argument" field'
        do: (
            moveTo: Arg1Editor
            click: ReceiverEditor
        )
        showing: 'Enter a value (or expression) into "receiver" field'
        do: (
            enter: '100'
        )
     )
END"

    "Created: / 19-07-2019 / 10:54:04 / Claus Gittinger"
    "Modified: / 19-07-2019 / 14:53:15 / Claus Gittinger"
! !

!ShowMeHowItWorks class methodsFor:'documentation'!

version_CVS
    ^ '$Header$'
! !