ShowMeHowItWorks.st
author Claus Gittinger <cg@exept.de>
Mon, 14 Oct 2019 10:48:32 +0200
changeset 3782 f26ed65a4360
parent 3781 0040ac44a2f3
child 3783 e935b92253c9
permissions -rw-r--r--
#UI_ENHANCEMENT by exept class: UIPainter changed: #processEvent:

"{ Encoding: utf8 }"

"{ Package: 'stx:libtool2' }"

"{ NameSpace: Smalltalk }"

Object subclass:#ShowMeHowItWorks
	instanceVariableNames:'application opStream lastComponentName lastComponent lastResult
		voice translate language verifying closeApplicationWhenFinished'
	classVariableNames:'IntroShownCount DebugMode'
	poolDictionaries:''
	category:'Interface-Help'
!

!ShowMeHowItWorks class methodsFor:'documentation'!

documentation
"
    automatic presentations.
    To see how it works, open a methodFinder:
        MethodFinderWindow open
    and select its 'Show me how it works' item in the help menu.

    [author:]
        Claus Gittinger
"
!

example
    MethodFinderWindow open.
    
    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'!

application:anApplicationOrNilForAll do:specArray 
    "spec contains a list of action commands (show: / moveTo: etc.)"

    self new 
        application:anApplicationOrNilForAll;
        do:specArray 

    "
     ShowMeHowItWorks do:
        #(
            (language: de)
            (show: 'üben üben üben')
            (wait: 0.5)
            (moveTo: NameOfComponent)
        )    
    "

    "Created: / 19-07-2019 / 10:52:59 / Claus Gittinger"
    "Modified (comment): / 23-07-2019 / 10:26:42 / Claus Gittinger"
!

do:specArray
    "spec contains a list of action commands (show: / moveTo: etc.)"

    self new do:specArray

    "
     ShowMeHowItWorks do:
        #(
            (language: de)
            (show: 'üben üben üben')
            (wait: 0.5)
            (moveTo: NameOfComponent)
        )    
    "

    "Created: / 19-07-2019 / 10:52:59 / Claus Gittinger"
    "Modified (comment): / 23-07-2019 / 10:26:42 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'accessing'!

application:anApplication
    "if set, only that application is presented (widget search is limtied to that one)"

    application := anApplication.
! !

!ShowMeHowItWorks methodsFor:'commands'!

intro
    <action>

    verifying ifTrue:[^ self].

    IntroShownCount := (IntroShownCount ? 0).

    "/ only say this a few times..
    IntroShownCount > 3 ifTrue:[^ self].
    IntroShownCount := IntroShownCount + 1.

    self language:(Smalltalk language).    
    self tell:(self class classResources 
                string:'You can stop this show, by pressing the SHIFT key').

    "Created: / 19-07-2019 / 15:49:19 / Claus Gittinger"
    "Modified: / 23-07-2019 / 11:06:13 / Claus Gittinger"
!

language:lang
    <action>

    voice := OperatingSystem bestVoiceForLanguage:lang.
    translate := false.
    language := lang.

    "Created: / 23-07-2019 / 10:27:02 / Claus Gittinger"
!

open:applicationClassName
    <action>

    |appClass|

    (appClass := Smalltalk classNamed:applicationClassName) isNil ifTrue:[
        self error:'no such application class'
    ].
    verifying ifFalse:[
        application := appClass new openAndWaitUntilVisible.
        closeApplicationWhenFinished := true.
    ].

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

pause
    <action>
    
    Dialog information:(self class classResources 
                            stringWithCRs:'Show Paused.\Click on "OK" to proceed')

    "Created: / 19-07-2019 / 15:03:17 / Claus Gittinger"
    "Modified: / 19-07-2019 / 16:13:33 / Claus Gittinger"
!

raise:what
    <action>

    verifying ifTrue:[^ self].

    what == #application ifTrue:[
        application topView raise.
        ^ self
    ].
    what == #masterApplication ifTrue:[
        application windowGroup isModal ifTrue:[
            application windowGroup mainGroup topViews first raise
        ] ifFalse:[
            (application masterApplication ? application) topViews first raise.
        ].
        ^ self
    ].
    self halt.
!

show:message
    "showing (and speak) some message."

    <action>
    
    self showing:message saying:nil do:nil

    "Created: / 19-07-2019 / 15:59:18 / Claus Gittinger"
    "Modified (comment): / 19-07-2019 / 18:54:36 / Claus Gittinger"
!

show:message for:seconds
    "showing (and speak) some message and wait for some time."

    <action>

    DebugMode == true ifTrue:[^ self].

    self show:message.
    self wait:seconds.

    "Created: / 19-07-2019 / 18:54:20 / Claus Gittinger"
!

show:message saying:sentenceOrNil
    "showing (and speak) some message."

    <action>
    
    self showing:message saying:sentenceOrNil do:nil

    "Created: / 19-07-2019 / 15:59:18 / Claus Gittinger"
    "Modified (comment): / 19-07-2019 / 18:54:36 / Claus Gittinger"
!

show:message saying:sentenceOrNil for:seconds
    "showing (and speak) some message and wait for some time."

    <action>
    
    DebugMode == true ifTrue:[^ self].
    self show:message saying:sentenceOrNil.
    self wait:seconds.

    "Created: / 19-07-2019 / 18:54:20 / Claus Gittinger"
!

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

    <action>
    
    self showing:message saying:nil do:operationsOrNothing
!

showing:message saying:sentenceOrNil do:operationsOrNothing
    "execute operationsOrNothing while showing (and speaking) some message."

    <action>
    
    |xLatedMessage messageView talkDone|

    xLatedMessage := (translate and:[application notNil]) 
                            ifTrue:[application resources string:message]
                            ifFalse:[message].
    
    self assert:(operationsOrNothing isNil or:[operationsOrNothing isSequenceable]).

    messageView := ActiveHelpView for:xLatedMessage.
    "/ messageView shapeStyle:#cartoon.
    [
        messageView origin:(Screen current pointerPosition + (0 @ 20)).
        messageView makeFullyVisible.
        messageView realize.

        self talking ifTrue:[
            talkDone := Semaphore new.
            [
                self tell:(sentenceOrNil ? xLatedMessage).
                talkDone signal
            ] fork.
            
            "/
            "/ allow speaker some headoff
            verifying ifFalse:[
                Delay waitForSeconds:(xLatedMessage size / 15)+0.5.
            ].
        ].

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

    "Created: / 19-07-2019 / 11:19:27 / Claus Gittinger"
    "Modified: / 23-07-2019 / 10:52:30 / Claus Gittinger"
!

thankyou
    <action>
    
    self show:(self randomThankYou).

    "Created: / 23-07-2019 / 10:50:43 / Claus Gittinger"
!

wait:seconds
    <action>
    
    verifying ifTrue:[^ self].
    DebugMode == true ifTrue:[^ self].
    Display ctrlDown ifTrue:[^ self].

    Delay waitForSeconds:seconds

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

waitFor:componentName timeout:seconds
    <action>

    |endTime|

    verifying ifTrue:[^ self].

    endTime := Timestamp now + seconds.
    [
        (self findComponent:componentName) notNil ifTrue:[
            ^ self
        ].
        Delay waitForSeconds:0.05.
        Timestamp now > endTime ifTrue:[
            self error:('component %1 not present after %2' bindWith:componentName with:seconds)
        ]
    ] loop
! !

!ShowMeHowItWorks methodsFor:'commands - checking'!

isEmpty:componentName
    <action>

    |component|
    
    component := self componentNamed:componentName.
    component isScrollWrapper ifTrue:[ component := component scrolledView ].
    component isTextView ifTrue:[
        ^ component contents isEmptyOrNil
    ] ifFalse:[
        self halt.
    ].
    self error:'isEmpty: unhandled component type: ',component displayString.

    "Created: / 19-07-2019 / 15:33:47 / Claus Gittinger"
!

unless:query do:actions
    <action>

    |result|
    
    result := self doCommand:query.
    result ifFalse:[
        self doCommand:actions
    ].

    "Created: / 19-07-2019 / 15:33:32 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'commands - mouse & keyboard'!

click
    "press-release"
    
    <action>

    ^ self click:1 inComponent:lastComponent

    "Created: / 19-07-2019 / 16:11:03 / Claus Gittinger"
!

click:buttonNr
    "press-release"
    
    <action>

    self assert:(buttonNr isInteger).
    ^ self click:buttonNr inComponent:lastComponent

    "Created: / 19-07-2019 / 13:21:20 / Claus Gittinger"
    "Modified: / 19-07-2019 / 16:10:19 / Claus Gittinger"
!

clickIn:componentName
    "press-release"
    
    <action>

    ^ self click:1 inComponent:(self componentNamed:componentName)

    "Created: / 19-07-2019 / 16:09:58 / Claus Gittinger"
!

fastMoveTo:componentName
    "move the mouse to componentName without circling"

    <action>

    |component|

    component := self componentNamed:componentName.
    self movePointerToComponent:component speed:(self pointerMoveSpeedFast).

    "
     ShowMeHowItWorks basicNew fastMoveTo:'Classes'
     ShowMeHowItWorks basicNew fastMoveTo:'Klassen'
    "

    "Created: / 19-07-2019 / 15:39:23 / Claus Gittinger"
    "Modified: / 20-07-2019 / 08:14:16 / Claus Gittinger"
    "Modified (comment): / 23-07-2019 / 09:33:31 / Claus Gittinger"
!

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

    <action>

    |component|

    component := self componentNamed:componentName.
    self movePointerToComponent:component.
    Display ctrlDown ifTrue:[^ self].
    self circlePointerAroundComponent:component.

    "Created: / 19-07-2019 / 11:20:42 / Claus Gittinger"
    "Modified: / 19-07-2019 / 15:38:11 / Claus Gittinger"
!

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

    <action>

    |idx|

    verifying ifTrue:[^ self].

    (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>

    self selectIndex:itemsIndex in:lastComponent

    "Created: / 19-07-2019 / 14:20:11 / Claus Gittinger"
    "Modified: / 19-07-2019 / 21:59:36 / Claus Gittinger"
!

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

    <action>

    |widget y offset possibleWidgets|

    verifying ifTrue:[^ self].

    (widget := widgetArg) isScrollWrapper ifTrue:[
        widget := widget scrolledView
    ].
    
    (widget isKindOf:ComboView) ifTrue:[
        "/ click on the menubutton
        self movePointerToComponent:widget menuButton.
        self click:1 inComponent:widget menuButton.
        Delay waitForSeconds:0.5.
        widget select:itemsIndex.
        Delay waitForSeconds:0.5.
self halt.
        ^ self
    ].    
    (widget isKindOf:SelectionInListView) ifTrue:[
        (widget isLineVisible:itemsIndex) ifFalse:[
            widget scrollToLine:itemsIndex
        ].    
        "/ click on the item
        y := widget yOfLine:itemsIndex.
        offset := (widget width // 2) @ y.
        self movePointerToComponent:widget offset:offset.
        widget simulateButtonPress:1 at:offset sendDisplayEvent:false.
        Delay waitForSeconds:(self clickTime).
        widget simulateButtonRelease:1 at:offset sendDisplayEvent:false.
        Delay waitForSeconds:0.5.
        ^ self
    ].
    (widget isKindOf:SelectionInListModelView) ifTrue:[
        (widget isLineVisible:itemsIndex) ifFalse:[
            widget scrollToLine:itemsIndex
        ].    
        y := widget yVisibleOfLine:itemsIndex.
        offset := (widget width // 2) @ y.
        self movePointerToComponent:widget offset:offset.
        widget simulateButtonPress:1 at:offset sendDisplayEvent:false.
        Delay waitForSeconds:(self clickTime).
        widget simulateButtonRelease:1 at:offset sendDisplayEvent:false.
        Delay waitForSeconds:0.5.
        ^ self
    ].

    "/ none of it - see what is in there
    possibleWidgets := OrderedCollection new.
    widget allSubViewsDo:[:each |
        ((each isKindOf:ComboView) 
          or:[(each isKindOf:SelectionInListView)
          or:[(each isKindOf:SelectionInListModelView)
        ]]) ifTrue:[
            possibleWidgets add:each
        ]
    ].
    possibleWidgets size == 1 ifTrue:[
        self selectIndex:itemsIndex in:(possibleWidgets first).
        ^ self
    ].    
    
    self error:'cannot select this component'

    "Created: / 19-07-2019 / 21:59:15 / Claus Gittinger"
    "Modified: / 20-07-2019 / 07:57:41 / Claus Gittinger"
!

type:aString
    "enter text into the last component"

    <action>

    |t|

    verifying ifTrue:[^ self].

    t := Display ctrlDown ifTrue:[0.05] ifFalse:[0.1].
    lastComponent 
        simulateTextInput:aString at:(lastComponent extent // 2) 
        sendDisplayEvent:false keyPressTime:t

    "Created: / 19-07-2019 / 15:50:40 / 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"

    ^ self shortClickTime

    "Created: / 19-07-2019 / 13:17:20 / Claus Gittinger"
    "Modified: / 19-07-2019 / 15:21:51 / Claus Gittinger"
!

longClickTime
    "when clicking buttons"

    ^ 500 milliseconds

    "Created: / 19-07-2019 / 15:21:42 / 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"
!

pointerMoveSpeedFast
    ^ 600.   "/ pixels per second

    "Created: / 20-07-2019 / 08:13:58 / Claus Gittinger"
!

shortClickTime
    "when clicking"

    ^ 100 milliseconds

    "Created: / 19-07-2019 / 15:21:29 / Claus Gittinger"
!

talking
    "/ DebugMode := true
    verifying ifTrue:[^ false].
    DebugMode == true ifTrue:[^ false].

    "/ ^ Expecco::ExpeccoPreferences current speechEffectsEnabled
    ^ true

    "Created: / 19-07-2019 / 14:31:14 / Claus Gittinger"
    "Modified (comment): / 23-07-2019 / 09:45:35 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'helper'!

randomThankYou
    ^ #(
        'thank you, for watching'
        'thank you for watching'
        'thank you'
        'thanks'
        'have a good day'
        'have a nice day'
        'have fun'
        'have fun with expecco'
        'have fun with expecco, by the way: expecco comes from the latin word: peccare, which means: "to sin"'
        'happy hacking'
        'happy hacking, I hope you liked what you saw'
        'hope you liked it'
        'see you again'
        'be the source with you'
        'be the force with you'
        'may the force be with you'
        'may the source be with you'
        'please give feedback, and let us know, if you liked it'
        'if you have any questions, please contact exept'
        'if you need more information, please take a look at the wiki'
    ) atRandom

    "
     OperatingSystem speak:'may the source be with you'
     OperatingSystem speak:'have fun with expecco'
     OperatingSystem speak:'have fun with expecco, by the way: expecco comes from the latin word: peccare, which means: to sin'
     OperatingSystem speak:'happy hacking, I hope you liked what you saw'
     OperatingSystem speak:'please give feedback, and let us know, if you liked it'
     OperatingSystem speak:'if you have any questions, please contact exept'
     OperatingSystem speak:'if you need more information, please take a look at the wiki'
    "

    "Created: / 19-07-2019 / 21:39:18 / Claus Gittinger"
!

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

    "Created: / 19-07-2019 / 14:57:50 / Claus Gittinger"
    "Modified: / 23-07-2019 / 10:28:02 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'helpers - broken'!

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

    verifying ifTrue:[^ self].

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

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

!ShowMeHowItWorks methodsFor:'helpers - component search'!

componentNamed:componentName
    "retrieve a component by name or report an error if not found.
     Can return either a view or a menu item"

    |component|

    lastComponentName := componentName.

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

    "Created: / 19-07-2019 / 15:37:35 / Claus Gittinger"
    "Modified (comment): / 23-07-2019 / 09:31:53 / Claus Gittinger"
!

findComponent:componentName
    "find a component by name - in the active and possibly in any app.
     Can return either a view or a menu item"
    
    |component candidates modalGroup|

    application notNil ifTrue:[ 
        (component := self findComponent:componentName in:application) notNil ifTrue:[
            ^ component.
        ].
    ].
    candidates := OrderedCollection new.

    "/ is there a modal dialog open for the app?
    (modalGroup := application windowGroup modalGroup) notNil ifTrue:[
        modalGroup topViews do:[:eachModalTopView |
            component := self findComponent:componentName in:eachModalTopView.
            component notNil ifTrue:[ 
                candidates add:component
            ].
        ].
        candidates size == 1 ifTrue:[
            ^ candidates first
        ].
    ].
    application isNil ifTrue:[
        "/ search through all current applications
        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:[
            ^ candidates first
        ].
        candidates notEmpty ifTrue:[
            "/ multiple elements (probably there are multiple topviews open...
            "/ check the current windowGroup
            self error:'multiple components found by name: ',componentName.
        ].
    ].

    ^ nil

    "
     ShowMeHowItWorks basicNew findComponent:'Classes'
     ShowMeHowItWorks basicNew findComponent:'Klassen'
    "

    "Created: / 19-07-2019 / 12:02:30 / Claus Gittinger"
    "Modified: / 19-07-2019 / 16:44:30 / Claus Gittinger"
    "Modified (comment): / 23-07-2019 / 09:32:44 / Claus Gittinger"
!

findComponent:componentNameOrPath in:anApplicationOrViewOrMenuItem
    "find a component by name inside an app or inside a view.
     Uses the NameKey of the spec, and optionally the label or modelKey.
     Can return either a view or a menu item"

    |idx app window component componentNameSymbol foundByName foundByTitle foundByLabel item
     checkIfAllMenuItemsDoTheSame|

    (componentNameOrPath includes:$/) ifTrue:[
        (idx := componentNameOrPath indexOf:$/) ~~ 0 ifTrue:[
            |containerName restPath container|

            containerName := componentNameOrPath copyTo:idx-1.
            restPath := componentNameOrPath copyFrom:idx+1.
            container := self findComponent:containerName in:anApplicationOrViewOrMenuItem.
            container isNil ifTrue:[ ^ nil ].
            ^ self findComponent:restPath in:container
        ]
    ].

    componentNameSymbol := componentNameOrPath asSymbolIfInterned ? componentNameOrPath.

    (app := anApplicationOrViewOrMenuItem) isView ifTrue:[
        window := anApplicationOrViewOrMenuItem.
        app := anApplicationOrViewOrMenuItem application
    ].
    app isApplicationModel ifTrue:[
        (component := app componentAt:componentNameSymbol) notNil ifTrue:[^ component].
        window := window ? app window.
    ] ifFalse:[
        app isMenuItem ifTrue:[
            window := app submenu.
        ]
    ].
    
    "/ 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.
    
    window withAllSubViewsDo:[:each |
        |foundIt|
        
        foundIt := false.
        each shown ifTrue:[
            [
                each name = componentNameSymbol ifTrue:[ foundByName add:each. foundIt := true ].
            ] on:MessageNotUnderstood do:[:ex | ].
            foundIt ifFalse:[
                [
                    each title = componentNameSymbol ifTrue:[ foundByTitle add:each. foundIt := true ].
                ] on:MessageNotUnderstood do:[:ex | ].
                foundIt ifFalse:[
                    [
                        each label = componentNameSymbol ifTrue:[ foundByLabel add:each. foundIt := true ].
                    ] on:MessageNotUnderstood do:[:ex | ].
                    foundIt ifFalse:[
                        each isMenu ifTrue:[
                            (item := each detectItemForNameKey:componentNameSymbol) notNil ifTrue:[
                                foundByName add:item. foundIt := true
                            ].
                            foundIt ifFalse:[
                                (item := each detectItemForKey:componentNameSymbol) notNil ifTrue:[
                                    foundByName add:item. foundIt := true 
                                ].    
                                foundIt ifFalse:[
                                    (item := each detectItemForLabel:componentNameSymbol) notNil ifTrue:[
                                        foundByLabel add:item. foundIt := true 
                                    ].    
                                ].
                            ].
                        ].    
                    ].
                ]    
            ].
        ].
    ].

    "/ a check, if multiple menu items have the same action, then choose the first found
    checkIfAllMenuItemsDoTheSame := 
        [:itemsFound |
            |visibleItems|

            (itemsFound conform:[:each | each askFor:#isMenuItem]) ifTrue:[
                (itemsFound collect:[:item | item itemValue]) asSet size == 1 ifTrue:[
                    "/ choose one which is visible, if possible
                    visibleItems := itemsFound select:[:item | item menuPanel shown].
                    visibleItems notEmpty ifTrue:[
                        ^ visibleItems first
                    ].
                    ^ itemsFound first.
                ].
            ].
        ].

    foundByName notEmpty ifTrue:[
        checkIfAllMenuItemsDoTheSame value:foundByName.
        self assert:(foundByName size == 1) message:'multiple components found by name'.
        ^ foundByName first.
    ].
    foundByTitle notEmpty ifTrue:[
        checkIfAllMenuItemsDoTheSame value:foundByTitle.
        self assert:(foundByTitle size == 1) message:'multiple components found by title'.
        ^ foundByTitle first.
    ].
    foundByLabel notEmpty ifTrue:[
        checkIfAllMenuItemsDoTheSame value:foundByLabel.
        self assert:(foundByLabel size == 1) message:'multiple components found by label'.
        ^ foundByLabel first.
    ].
    ^ component

    "
     self basicNew findComponent:'Klassen' in:(Transcript topView)
    "

    "Created: / 19-07-2019 / 11:36:21 / Claus Gittinger"
    "Modified (comment): / 23-07-2019 / 09:31:34 / Claus Gittinger"
!

screenBoundsOfComponent:aWidgetOrMenuItem
    aWidgetOrMenuItem isView ifTrue:[
        ^ aWidgetOrMenuItem screenBounds
    ].
    (aWidgetOrMenuItem askFor:#isMenuItem) ifTrue:[
        |menuPanel menuBounds|

        aWidgetOrMenuItem isVisible ifTrue:[
            aWidgetOrMenuItem layout notNil ifTrue:[
                menuPanel := aWidgetOrMenuItem menuPanel.
                menuPanel shown ifTrue:[
                    menuBounds := menuPanel screenBounds.
                    ^ aWidgetOrMenuItem layout + menuBounds origin 
                ].
            ].
        ].
    ].
    ^ nil.
! !

!ShowMeHowItWorks methodsFor:'helpers - mouse buttons'!

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

    |t|

    t := self shortClickTime.
    (component isKindOf:Button) ifTrue:[
        t := self longClickTime
    ].    
    self click:buttonNr inComponent:component clickTime:t

    "Created: / 19-07-2019 / 13:18:27 / Claus Gittinger"
    "Modified: / 19-07-2019 / 15:22:47 / Claus Gittinger"
!

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

    |viewToClick clickPos|

    self assert:viewOrMenuItem notNil.
    verifying ifTrue:[^ self].

    (viewOrMenuItem askFor:#isMenuItem) ifTrue:[
        viewToClick := viewOrMenuItem menuPanel.
        clickPos := viewOrMenuItem layout center.
        self assert:(clickPos notNil).
    ] ifFalse:[
        viewToClick := viewOrMenuItem.
        clickPos := viewToClick extent // 2.
    ].

    viewToClick simulateButtonPress:buttonNr at:clickPos sendDisplayEvent:false.
    Delay waitForSeconds:clickTime.
    viewToClick simulateButtonRelease:buttonNr at:clickPos sendDisplayEvent:false.

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

    "Created: / 19-07-2019 / 15:21:05 / Claus Gittinger"
!

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

    verifying ifTrue:[^ self].

    screen := Screen current.
    position := screen pointerPosition.
    x := position x.
    y := position y.
    
    self movePointerToScreenPosition: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"
    "Modified: / 23-07-2019 / 09:38:31 / Claus Gittinger"
!

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

    verifying ifTrue:[^ self].

    screen := Screen current.
    position := screen pointerPosition.
    x := position x.
    y := position y.
    
    self movePointerToScreenPosition: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"
    "Modified: / 23-07-2019 / 09:38:38 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'helpers - mouse movement'!

circlePointerAroundComponent:aWidgetOrMenuItem
    "circle around it a few times"

    |bounds position|

    bounds := self screenBoundsOfComponent:aWidgetOrMenuItem.
    bounds isNil ifTrue:[
        self error:'no bounds found for: ',aWidgetOrMenuItem printString.
    ].
    position := bounds center rounded.
    self circlePointerAroundScreenPosition:position

    "Created: / 19-07-2019 / 13:12:35 / Claus Gittinger"
    "Modified: / 23-07-2019 / 09:38:12 / Claus Gittinger"
!

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

    verifying ifTrue:[^ self].

    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: / 23-07-2019 / 09:37:46 / Claus Gittinger"
!

fastMovePointerToScreenPosition:position
    self movePointerToScreenPosition:position speed:(self pointerMoveSpeedFast).

    "Created: / 23-07-2019 / 09:36:20 / Claus Gittinger"
!

movePointerToComponent:aWidgetOrMenuItem
    "move the mouse to aWidget's center"

    |bounds|

    bounds := self screenBoundsOfComponent:aWidgetOrMenuItem.
    self movePointerToScreenPosition:(bounds center rounded).

    "Created: / 19-07-2019 / 13:11:33 / Claus Gittinger"
    "Modified: / 23-07-2019 / 09:37:01 / Claus Gittinger"
!

movePointerToComponent:aWidgetOrMenuItem offset:offset
    "move the mouse to position inside aWidget's"
    
    |bounds|

    bounds := self screenBoundsOfComponent:aWidgetOrMenuItem.
    self movePointerToScreenPosition:(bounds origin + offset) rounded.

    "Created: / 19-07-2019 / 16:18:58 / Claus Gittinger"
    "Modified: / 23-07-2019 / 09:36:57 / Claus Gittinger"
!

movePointerToComponent:aWidgetOrMenuItem speed:pixelsPerSecond
    "move the mouse to aWidget's center"

    |bounds position|
    
    bounds := self screenBoundsOfComponent:aWidgetOrMenuItem.
    bounds isNil ifTrue:[
        self error:'no bounds found for: ',aWidgetOrMenuItem printString.
    ].
    position := bounds center rounded.
    self movePointerToScreenPosition:position speed:pixelsPerSecond.

    "Created: / 20-07-2019 / 08:12:49 / Claus Gittinger"
    "Modified: / 23-07-2019 / 09:37:27 / Claus Gittinger"
!

movePointerToScreenPosition:newPosition
    "move the mouse to newPosition"

    self movePointerToScreenPosition:newPosition speed:(self pointerMoveSpeed)

    "Created: / 23-07-2019 / 09:36:39 / Claus Gittinger"
!

movePointerToScreenPosition:newPosition speed:pixelsPerSecond
    "move the mouse to newPosition, which is a screen position"
    
    |screen distance start numSteps moveTime stepDelayTime delta|

    verifying ifTrue:[^ self].

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

    distance := start dist:newPosition.
    moveTime := (distance / pixelsPerSecond) 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: / 23-07-2019 / 09:36:45 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'running'!

do:specArray
    "must run as a separate process;
     otherwise - if started by the app itself -
     no events will be processed while running"

    |wasActive|

    (wasActive := ActiveHelp isActive) ifTrue:[
        ActiveHelp stop.
    ].

    language isNil ifTrue:[
        self language:(Smalltalk language).
    ].
    translate := false.
    
    application isNil ifTrue:[
        application := WindowGroup activeMainApplication.
    ].
    closeApplicationWhenFinished := false.

    "/ run in verifying mode
    verifying := true.
    [
        Error handle:[:ex |
            Transcript showCR:'Possible error (encountered while verifying):\\%1\\Debug?' with:ex description.
            Display ctrlDown ifTrue:[ex reject].
        ] do:[
            self doStream:(specArray readStream)
        ].
    ] ensure:[
        verifying := false.
        wasActive ifTrue:[ActiveHelp start].
    ].

    [
        ActiveHelp stop.
        [
            Error handle:[:ex |
                Dialog warn:(self class classResources stringWithCRs:'An error was encountered in the show:\\%1' with:ex description)
            ] do:[
                self doStream:(specArray readStream)
            ].
        ] ensure:[
            wasActive ifTrue:[ActiveHelp start].
        ].
    ] fork.

    "
     ShowMeHowItWorks do:
        #(
            (show: 'bla bla')
            (moveTo: NameOfComponent)
        )    
    "

    "Created: / 23-07-2019 / 10:24:53 / Claus Gittinger"
    "Modified: / 25-07-2019 / 11:48:53 / Claus Gittinger"
! !

!ShowMeHowItWorks methodsFor:'running - private'!

doCommand:op
    "execute a single command"
    
    |numArgs sel args method|

    op isArray ifTrue:[
        op first isArray ifTrue:[
            self doStream:op readStream.
            ^ self.
        ].
        
        "/ 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].
    
    lastResult := self perform:sel withArguments:args.
    ^ lastResult
    
"<<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 / 15:34:55 / Claus Gittinger"
    "Modified: / 23-07-2019 / 09:14:26 / Claus Gittinger"
!

doStream:specStream
    |previousStream resources|

    resources := self class classResources.
    
    previousStream := opStream.
    [
        opStream := specStream.
        [opStream atEnd] whileFalse:[
            self nextCommand.
            Display shiftDown ifTrue:[
                (IntroShownCount ? 0) > 3 ifFalse:[
                    self tell:(self possiblyTranslate:'You pressed the SHIFT key.').
                ].    
                self tell:(self possiblyTranslate:'Do you want to stop the show?').
                (Dialog confirm:(resources stringWithCRs:'Stop the demonstration?'))
                ifTrue:[
                    self tell:(self possiblyTranslate:'OK,').
                    self tell:(self possiblyTranslate:(self randomThankYou)).
                    ^ 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: / 23-07-2019 / 11:48:45 / Claus Gittinger"
!

nextCommand
    self doCommand:(opStream next).

"<<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 / 15:35:15 / Claus Gittinger"
!

possiblyTranslate:aString
    translate ifTrue:[^ self class classResources string:aString].
    ^ aString

    "Created: / 23-07-2019 / 11:48:17 / Claus Gittinger"
! !

!ShowMeHowItWorks class methodsFor:'documentation'!

version_CVS
    ^ '$Header$'
! !