ActiveHelp.st
author Claus Gittinger <cg@exept.de>
Wed, 19 Aug 1998 15:38:41 +0200
changeset 1048 84719be68422
parent 1033 3a56ba930052
child 1100 5f25636fad8c
permissions -rw-r--r--
catch error while doing active help - since this is done by the event dispatcher, we dont want it to block our system.

"
 COPYRIGHT (c) 1995 by Claus Gittinger
	      All Rights Reserved

 This software is furnished under a license and may be used
 only in accordance with the terms of that license and with the
 inclusion of the above copyright notice.   This software may not
 be provided or otherwise made available to, or used by, any
 other person.  No title to or ownership of the software is
 hereby transferred.
"


EventListener subclass:#ActiveHelp
	instanceVariableNames:'currentView currentHelpView currentFrame showProcess closeProcess
		lastHelpText listeningForAll applicationsOrTopViewsWithHelp'
	classVariableNames:'DelayTime ShowTime TheOneAndOnlyHelpListener'
	poolDictionaries:''
	category:'Interface-Help'
!

!ActiveHelp class methodsFor:'documentation'!

copyright
"
 COPYRIGHT (c) 1995 by Claus Gittinger
	      All Rights Reserved

 This software is furnished under a license and may be used
 only in accordance with the terms of that license and with the
 inclusion of the above copyright notice.   This software may not
 be provided or otherwise made available to, or used by, any
 other person.  No title to or ownership of the software is
 hereby transferred.
"

!

documentation
"
    The active help listener.

    The one and only instance of myself intercepts incoming mouse & keyboard 
    events for the display device, being especially interested in view-enter/
    leave events. When such an event arrives, it asks the corresponding view
    or its model for a help message and display it via an ActiveHelpView.
    (actually, the view is first asked if it would like to display it itself
     - for example, in some information-view).

    The query for the helpText is repeated along the views superView chain, 
    until any model or view returns a nonNil answer for the #helpTextFor:at 
    or #helpTextFor message.

    All I need for automatic help is some model/view/applicationModel along
    the superview chain of the entered component, which responds to the
    #helpTextFor: message with a non-nil (string-) answer.
    I close down the help view after a while, if a key is pressed or the mouse
    moved to another view.

    Who should provide the helpText:

        the best place is the application object (an instance of ApplicationModel)
        or the topView, if its a derived class of StandardSystemView.
        This should know about its components and return the string
        when asked via #helpTextFor:aComponent.
        See examples in FileBrowser, Launcher etc.

    Be aware, that for applicationModels, there must be a link from the
    topView to this applicationModel 
    (set via: aTopView application:anApplicationModel)
    otherwise, the helpManager has no means of finding the application which
    corresponds to a view.

    Who should display the helpText:

        by default, the helpListener opens a little popup view, which displays the
        returned help message. However, a nice trick which can be used by applications
        is to create an infoLabel as a subview of the topFrame (a la windows)
        and display the text right in the #helpTextFor: method. To cheat the
        help listener, this method should then return nil, to keep it silent.


    Usage:
        If help is to be shown for all views (as enabled by the launchers help menu),
        use 'ActiveHelp start' and 'ActiveHelp stop'.

        Individual apps may enable/disable active help for themself by:
        'ActiveHelp startFor:app' or 'ActiveHelp stopFor:app', passing either
        the topView or the topViews application as argument.
        This is usually done by applications which want to show contextHelp in
        some infoView.

        
    [author:]
        Claus Gittinger

    [start with:]
        ActiveHelp start
        ActiveHelp stop

    [See also:]
        ActiveHelpView
        WindowGroup WindowEvent
        ApplicationModel StandardSystemView
"
!

examples
"
  Active Help for a single view
  or app (whatever the global settings are):
  Can be initiated by an app when its opened.
                                                                        [exBegin]
        |app top myAppClass|

        Class withoutUpdatingChangesDo:[
            myAppClass := ApplicationModel 
                            subclass:#'Demos::DemoApp'
                            instanceVariableNames:''
                            classVariableNames:''
                            poolDictionaries:''
                            category:'demos'.

            myAppClass 
                compile:'helpTextFor:aView    Transcript showCR:''hello''. ^ ''this is some helpText'''.

        ].
        app := myAppClass new.

        top := StandardSystemView new.
        top extent:300@100.
        top application:app.
        top open.

        ActiveHelp startFor:app.

        Class withoutUpdatingChangesDo:[
            myAppClass removeFromSystem
        ]
                                                                        [exEnd]


  Active Help (for all views):

    (make certain that activeHelp is turned on ...
     ... otherwise, you will see nothing)

    the following example uses a Plug as a model replacement.
    In concrete application, you would create a method to implement the helpText
    query message.
                                                                        [exBegin]
        |app top button1 button2|

        app := Plug new.
        app respondTo:#helpTextFor:
                 with:[:view | 
                               view == button1 ifTrue:[
                                 'this is button1'
                               ] ifFalse:[
                                 view == button2 ifTrue:[
                                   'some help for button2'
                                 ] ifFalse:[
                                   nil
                                 ]
                               ]
                      ].

        top := StandardSystemView new.
        top extent:300@100.
        button1 := Button label:'b1' in:top.
        button1 origin:0.0@0.0 corner:0.5@30. 
        button2 := Button label:'b2' in:top.
        button2 origin:0.5@0.0 corner:1.0@30.
        top model:app. '<-- normally this would be: top application:app'.
        top open
                                                                        [exEnd]

    (make certain that activeHelp is turned on ...
     ... otherwise, you will see nothing)

    alternatively, display of the helpMessage in a local, private view:
                                                                        [exBegin]
        |app top button1 button2 infoView|

        app := Plug new.
        app respondTo:#helpTextFor:
                 with:[:view | infoView label:'info ...'.
                               view == button1 ifTrue:[
                                 infoView label:'this is button1'
                               ].
                               view == button2 ifTrue:[
                                 infoView label:'some help for button2'
                               ].
                               nil
                      ].

        top := StandardSystemView new.
        top extent:300@100.
        button1 := Button label:'b1' in:top.
        button1 origin:0.0@0.0 corner:0.5@30. 
        button2 := Button label:'b2' in:top.
        button2 origin:0.5@0.0 corner:1.0@30.
        infoView := Label label:'info ...' in:top.
        infoView level:-1; origin:0.0@1.0 corner:1.0@1.0.
        infoView topInset:(infoView preferredExtent y negated - 3);
                 leftInset:3; 
                 rightInset:3; 
                 bottomInset:3;
                 adjust:#left.
        top model:app. '<-- normally this would be: top application:app'.
        top open
                                                                        [exEnd]
"
! !

!ActiveHelp class methodsFor:'initialization'!

initialize
    "set default delay & help-display times"

    ShowTime := 15.
    DelayTime := 2.

    "
     ActiveHelp initialize
    "

    "Modified: 27.4.1996 / 15:07:27 / cg"
! !

!ActiveHelp class methodsFor:'queries'!

currentHelpListener
    "return the activeHelp listener if activeHelp is turned on, nil otherwise"

    ^ TheOneAndOnlyHelpListener

    "Created: 28.6.1997 / 13:59:44 / cg"
!

isActive
    "return true, if activeHelp is turned on"

    ^ TheOneAndOnlyHelpListener notNil

    "Modified: 27.4.1996 / 15:07:57 / cg"
! !

!ActiveHelp class methodsFor:'start & stop'!

start
    "start activeHelp for all apps"

    TheOneAndOnlyHelpListener isNil ifTrue:[
        TheOneAndOnlyHelpListener := self new.
    ].
    TheOneAndOnlyHelpListener listenForAll

    "
     ActiveHelp start
    "

    "Modified: / 26.10.1997 / 23:16:54 / cg"
!

startFor:anApplicationOrTopView
    "start activeHelp for a single app"

    TheOneAndOnlyHelpListener isNil ifTrue:[
        TheOneAndOnlyHelpListener := self new.
    ].
    TheOneAndOnlyHelpListener listenFor:anApplicationOrTopView

    "Modified: / 26.10.1997 / 23:17:05 / cg"
!

stop
    "stop activeHelp for all (except for individual apps)"

    TheOneAndOnlyHelpListener notNil ifTrue:[
        TheOneAndOnlyHelpListener unlistenAll.
    ].

    "
     ActiveHelp stop
    "

    "Modified: / 26.10.1997 / 23:18:58 / cg"
!

stopFor:anAppOrTopView
    "stop activeHelp for a single app"

    TheOneAndOnlyHelpListener notNil ifTrue:[
        TheOneAndOnlyHelpListener unlistenFor:anAppOrTopView.
    ].

    "Modified: / 26.10.1997 / 23:12:55 / cg"
    "Created: / 26.10.1997 / 23:18:41 / cg"
! !

!ActiveHelp class methodsFor:'times'!

delayTime:numberOfSeconds
    "set the delay (the time, the cursor has to be in the view
     before help is shown). The default is 2 seconds."

    DelayTime := numberOfSeconds

    "
     ActiveHelp delayTime:0.5
     ActiveHelp delayTime:2
     ActiveHelp delayTime:10
    "
!

showTime:numberOfSeconds
    "set the number of seconds, a help messages is to be shown.
     The default is 15 seconds."

    ShowTime := numberOfSeconds

    "
     ActiveHelp showTime:10
     ActiveHelp showTime:99999 
     ActiveHelp showTime:30
    "
! !

!ActiveHelp methodsFor:'listening'!

buttonMotion:state x:x y:y view:aView
    "handle motion events - if the mousepointer left the 
     previous helped view, hide the help"

    Object errorSignal handle:[:ex |
        ex return
    ] do:[
        "/ ignore my own help-view
        aView topView == currentHelpView ifTrue:[
           ^ true
        ].

        (self interestedIn:aView) ifTrue:[
            self stopHelpDisplayProcess.
            self hideIfPointerLeft:aView.
            self initiateHelpFor:aView atX:x y:y.
        ].
    ].
    ^ false

    "Modified: / 28.7.1998 / 02:51:51 / cg"
!

buttonPress:state x:x y:y view:view
    "handle button press - unconditionally hide the help"

    Object errorSignal handle:[:ex |
        ex return
    ] do:[
        self hideHelp.
    ].
    ^ false

    "Modified: / 28.7.1998 / 02:52:00 / cg"
!

keyPress:state x:x y:y view:view
    "handle key press - unconditionally hide the help"

    Object errorSignal handle:[:ex |
        ex return
    ] do:[
        self hideHelp.
    ].
    ^ false

    "Modified: 27.4.1996 / 15:09:57 / cg"
!

pointerEnter:state x:x y:y view:aView
    "handle pointer entering a view; setup timeOut to show help"

    "/ ignore my own help-view
    aView topView == currentHelpView ifTrue:[
       ^ true
    ].

    Object errorSignal handle:[:ex |
        ex return
    ] do:[
        (self interestedIn:aView) ifTrue:[
            self stopHelpDisplayProcess.
            self hideIfPointerLeft:aView.
            self initiateHelpFor:aView atX:x y:y.
        ].
    ].
    ^ false

    "Modified: / 28.7.1998 / 02:52:09 / cg"
!

pointerLeave:state view:view
    "handle pointer leaving a view; hide help text"

    Object errorSignal handle:[:ex |
        ex return
    ] do:[
        self hideIfPointerLeft:view.
    ].
    ^ false

    "Modified: / 28.7.1998 / 02:52:16 / cg"
! !

!ActiveHelp methodsFor:'private'!

helpTextFor:aView atX:x y:y
    "retrieve helptext for aView as a string; 
     walk along the views superView chain,
     asking models and views encountered while walking.
     The first one who understands and returns a nonNil answer to the
     #helpTextFor:at: or #helpTextFor: message ends this search and the
     returned string is returned."

    |model app text view v sv|

    view := aView.
    (model := aView model) notNil ifTrue:[
        (model respondsTo:#helpTextFor:at:) ifTrue:[
            text := model helpTextFor:aView at:x@y.
            text notNil ifTrue:[^ text].
        ].
        (model respondsTo:#helpTextFor:) ifTrue:[
            text := model helpTextFor:aView.
            text notNil ifTrue:[^ text].
        ]
    ].

    (aView respondsTo:#helpTextAt:) ifTrue:[
        text := aView helpTextAt:x@y.
        text notNil ifTrue:[^ text].
    ].
    (aView respondsTo:#helpText) ifTrue:[
        text := aView helpText.
        text notNil ifTrue:[^ text].
    ].

    "walk up the chain - maybe someone knows about its subview ..."
    v := aView.

    [(sv := v container) notNil] whileTrue:[
        (model := sv model) notNil ifTrue:[
            (model respondsTo:#helpTextFor:at:) ifTrue:[
                text := model helpTextFor:aView at:x@y.
                text notNil ifTrue:[^ text].
            ].
            (model respondsTo:#helpTextFor:) ifTrue:[
                text := model helpTextFor:aView.
                text notNil ifTrue:[^ text].
            ]
        ].

        (sv respondsTo:#helpTextFor:) ifTrue:[
            text := sv helpTextFor:aView.
            text notNil ifTrue:[^ text].
            text := sv helpTextFor:v.
            text notNil ifTrue:[^ text].
         ].

         v := sv.
    ].

    (aView respondsTo:#application) ifTrue:[
        (app := aView application) notNil ifTrue:[
            (app respondsTo:#helpTextFor:at:) ifTrue:[
                text := app helpTextFor:aView at:x@y.
                text notNil ifTrue:[^ text].
            ].
            (app respondsTo:#helpTextFor:) ifTrue:[
                text := app helpTextFor:aView.
                text notNil ifTrue:[^ text].
            ]
        ]
    ].

    (v notNil and:[v respondsTo:#application]) ifTrue:[
        (app := v application) notNil ifTrue:[
            (app respondsTo:#helpTextFor:at:) ifTrue:[
                text := app helpTextFor:aView at:x@y.
                text notNil ifTrue:[^ text].
            ].
            (app respondsTo:#helpTextFor:) ifTrue:[
                text := app helpTextFor:aView.
                text notNil ifTrue:[^ text].
            ]
        ]
    ].
    (v notNil and:[v respondsTo:#model]) ifTrue:[
        (model := v model) notNil ifTrue:[
            (model respondsTo:#helpTextFor:at:) ifTrue:[
                text := model helpTextFor:aView at:x@y.
                text notNil ifTrue:[^ text].
            ].
            (model respondsTo:#helpTextFor:) ifTrue:[
                text := model helpTextFor:aView.
                text notNil ifTrue:[^ text].
            ]
        ]
    ].

    (view class respondsTo:#helpText) ifTrue:[
        text := view class helpText.
        text notNil ifTrue:[^ text].
    ].

    ^ nil

    "Modified: / 31.8.1995 / 20:38:00 / claus"
    "Modified: / 28.7.1998 / 01:47:27 / cg"
!

hideIfPointerLeft:aView
    "hide help, if the pointer is not in aView"

    |whereOnScreen|

    whereOnScreen := aView graphicsDevice pointerPosition.

    (currentFrame notNil
    and:[(currentFrame insetBy:1@1) containsPoint:whereOnScreen]) ifFalse:[
        self hideHelp.
        currentView := nil
    ].

    "Modified: 28.5.1996 / 20:18:28 / cg"
!

interestedIn:aView
    "return true, if I am interested in aView (either listeningForAll,
     or in my list of apps)"

    |app|

    listeningForAll == true ifTrue:[^ true].
    aView isNil ifTrue:[^ false].

    (applicationsOrTopViewsWithHelp includesIdentical:(aView topView)) ifTrue:[^ true].
    app := aView topView application.
    app notNil ifTrue:[
        (applicationsOrTopViewsWithHelp includesIdentical:app) ifTrue:[^ true]
    ].
    ^ false

    "Created: / 26.10.1997 / 23:28:52 / cg"
    "Modified: / 8.8.1998 / 13:36:19 / cg"
!

stopHelpDisplayProcess
    |p|

    showProcess notNil ifTrue:[
        p := showProcess. showProcess := nil.
        p terminate.
    ].

    "Created: 28.6.1997 / 14:03:17 / cg"
! !

!ActiveHelp methodsFor:'show / hide help'!

hideHelp
    "hide the help text"

    |p|

    self stopHelpDisplayProcess.

    currentHelpView notNil ifTrue:[
        [
            currentHelpView destroy.
            currentHelpView := nil.
            currentView := nil.
        ] valueUninterruptably
    ].
    currentFrame := nil.
    closeProcess notNil ifTrue:[
        p := closeProcess. closeProcess := nil.
        p terminate.
    ]

    "Modified: 28.6.1997 / 14:03:50 / cg"
!

initiateHelpFor:aView atX:x y:y
    "ask aView for helpText, passing x/y coordinates;
     start a timeout process to display this helpText after some delay;
     Normally used internally, but can also be used by widgets to force 
     re-negotiation of the displayed helpText 
     (for example in a menu, when the selection changes)"

    self
        initiateHelpFor:aView atX:x y:y now:false

    "Modified: 28.6.1997 / 14:45:57 / cg"
!

initiateHelpFor:aView atX:x y:y now:showItNow
    "ask aView for helpText, passing x/y coordinates;
     start a timeout process to display this helpText after some delay;
     Normally used internally, but can also be used by widgets to force 
     re-negotiation of the displayed helpText 
     (for example in a menu, when the selection changes)"

    |text top app|

    text := self helpTextFor:aView atX:x y:y.
    lastHelpText = text ifTrue:[
        ^ self
    ].

    "/ give the views application a chance
    "/ to decide where to show the help text
    "/ (i.e. in its own information area)
    "/ nil is also passed down, to give it a chance
    "/ to clean its infoDisplay.

    top := aView topView.
    (app := aView application) notNil ifTrue:[
        (app showActiveHelp:text for:aView) ifTrue:[
            lastHelpText := text.
            ^ self
        ]
    ].

    text notNil ifTrue:[
        (showItNow not and:[DelayTime > 0]) ifTrue:[
            self stopHelpDisplayProcess.
            showProcess := [
                    Delay waitForSeconds:DelayTime.
                    showProcess := nil.
                    self showHelp:text for:aView
            ] forkAt:(Processor userSchedulingPriority + 1).
        ] ifFalse:[
            self showHelp:text for:aView
        ]
    ].

    "
     can be used in a widget as:

        ActiveHelp isActive ifTrue:[
            ActiveHelp currentHelpListener
                initiateHelpFor:self atX:x y:y
            ]
        ]
    "

    "Created: / 28.6.1997 / 14:45:41 / cg"
    "Modified: / 31.7.1998 / 18:25:28 / cg"
!

showHelp:aHelpText for:view
    "show the help text for aView"

    |org p v dev|

    view == currentView ifTrue:[
        lastHelpText = aHelpText ifTrue:[
            ^ self
        ]
    ].

    lastHelpText := aHelpText.

    closeProcess notNil ifTrue:[
        p := closeProcess. closeProcess := nil.
        p terminate.
    ].
    currentHelpView notNil ifTrue:[
        self hideHelp
    ].

    org := view originRelativeTo:nil.
    currentFrame := org extent:view extent.
    org :=org + (view extent // 2).

    v := ActiveHelpView for:aHelpText withCRs.

    dev := view graphicsDevice.
    org := dev pointerPosition.
    org := org + (10@10).
    (org x + v width) > dev width ifTrue:[
        org := (org x - v width) @ org y
    ].
    (org y + v height) > dev height ifTrue:[
        org := org x @ (org y - v height).
    ].

    v origin:org.
"/    currentHelpView open.
    v realize.
    v enableButtonMotionEvents.
    v enableMotionEvents.
    currentHelpView := v.

    currentView := view.
    closeProcess := [
        [
            (Delay forSeconds:ShowTime) wait.
            [
                currentHelpView notNil ifTrue:[
                    currentHelpView destroy.
                    currentHelpView := nil.
                ]
            ] valueUninterruptably
        ] valueOnUnwindDo:[
            closeProcess := nil.
        ].
    ] forkAt:(Processor userSchedulingPriority + 1).

    "Modified: 31.8.1995 / 19:20:45 / claus"
    "Modified: 28.6.1997 / 14:45:15 / cg"
! !

!ActiveHelp methodsFor:'start & stop'!

listenFor:anAppOrTopView
    "start listening"

    applicationsOrTopViewsWithHelp isNil ifTrue:[
        applicationsOrTopViewsWithHelp := WeakIdentitySet new.
    ].
    applicationsOrTopViewsWithHelp add:anAppOrTopView.
    super listen.

    "Created: / 26.10.1997 / 23:20:47 / cg"
    "Modified: / 26.10.1997 / 23:21:10 / cg"
!

listenForAll
    "start listening"

    listeningForAll := true.
    super listen.

    "Modified: / 28.6.1997 / 15:07:02 / cg"
    "Created: / 26.10.1997 / 23:19:30 / cg"
!

unlistenAll
    "stop listening"

    self hideHelp.
    listeningForAll := false.
    applicationsOrTopViewsWithHelp size == 0 ifTrue:[
        super unlisten.
    ]

    "Created: / 26.10.1997 / 23:14:17 / cg"
    "Modified: / 26.10.1997 / 23:23:04 / cg"
!

unlistenFor:anApp
    "stop listening for an app"

    self hideHelp.
    applicationsOrTopViewsWithHelp remove:anApp ifAbsent:nil.
    listeningForAll == true ifFalse:[
        applicationsOrTopViewsWithHelp size == 0 ifTrue:[
            super unlisten.
        ]
    ]

    "Created: / 26.10.1997 / 23:22:42 / cg"
    "Modified: / 29.10.1997 / 15:48:34 / cg"
! !

!ActiveHelp class methodsFor:'documentation'!

version
    ^ '$Header: /cvs/stx/stx/libview2/ActiveHelp.st,v 1.35 1998-08-19 13:38:41 cg Exp $'
! !
ActiveHelp initialize!