EditTextViewCompletionSupport.st
author Claus Gittinger <cg@exept.de>
Thu, 09 Nov 2017 20:09:30 +0100
changeset 6225 0122e4e6c587
parent 6197 3a3a1cabb663
child 6315 1362b6366e09
permissions -rw-r--r--
#FEATURE by cg class: GenericToolbarIconLibrary class added: #hideFilter16x16Icon

"{ Package: 'stx:libwidg' }"

"{ NameSpace: Smalltalk }"

Object subclass:#EditTextViewCompletionSupport
	instanceVariableNames:'completionView completionProcess editView autoSelect
		editViewLostFocusBlock'
	classVariableNames:'LastCompletions'
	poolDictionaries:''
	category:'Views-Text'
!

!EditTextViewCompletionSupport class methodsFor:'documentation'!

documentation
"
    An abstract supperclass to support completion in text views.
    Individual completion engines may create a subclass of 
    EditTextCompletionSupport and customize it.

    Basically, they have to implement #computeCompletions

    [author:]
        Claus Gittinger

    [instance variables:]

    [class variables:]

    [see also:]

"
! !

!EditTextViewCompletionSupport class methodsFor:'instance creation'!

for:anEditView
    ^ self new editView:anEditView
! !

!EditTextViewCompletionSupport class methodsFor:'queries'!

isAbstract
    ^ self == EditTextViewCompletionSupport

    "Created: / 26-09-2013 / 16:22:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!EditTextViewCompletionSupport methodsFor:'accessing'!

editView:anEditTextView
    editView := anEditTextView.
! !

!EditTextViewCompletionSupport methodsFor:'events'!

buttonPress:button x:x y:y
    self closeCompletionView.
!

editViewLostFocus
    "this is a hack for Windows:
     on windows, an activate:false event is first sent to my textView,
     then an activate is sent to the completion popup.
     this is done BEFORE the buttonPress event is delivered.
     therefore, allow for the activate of the completionMenu and its button event to be processed.
     before forcing it to be closed..."

    completionView notNil ifTrue:[
        editView graphicsDevice anyButtonPressed ifTrue:[
            editView sensor pushUserEvent:#editViewLostFocus for:self.
        ] ifFalse:[
            self closeCompletionView
        ]
    ].
!

handleKeyPress:key x:x y:y
    "return true, if I have eaten this keypress"

    |ch completeImmediate eatCursorLeftOrRight eatCursorUpDown|

    "/ completeImmediate := UserPreferences current immediateCodeCompletion.

    "/ open on CTRL- or TAB-key?
    (completionView isNil or:[completionView realized not]) ifTrue:[
        editView hasSelection ifFalse:[
            ((ch := editView characterBeforeCursor) notNil "/ i.e. not at begin of line
            and:[ ch isLetterOrDigit or:[ ch == $_ ] ]) ifTrue:[
                (key == #Control_L or:[ key == #Ctrl or:[ key == #Control_R or:[ key == #Control]]]) ifTrue:[
                    UserPreferences current codeCompletionOnControlKey ifTrue:[
                        autoSelect := true.
                        self updateCompletionList
                    ]
                ].
                ((key == #Tab) and:[editView sensor shiftDown not]) ifTrue:[
                    UserPreferences current codeCompletionOnTabKey ifTrue:[
                        autoSelect := true.
                        self updateCompletionList.
                        ^ true
                    ].
                ]
            ].
        ].
        ^ false.
    ].

    editView cursorCol <= 1 ifTrue:[^ false].
    
    "/ key for completion view ? (careful: do not forward too many, it would disturb user's typing)
    key isCharacter ifFalse:[
        "/ forward to menu
        (completionView notNil) ifTrue:[
            "/never
            eatCursorLeftOrRight := false.
"/                                    completeImmediate not
"/                                    or:[ editView sensor shiftDown 
"/                                    or:[ editView sensor ctrlDown ]].
            "/ only with shift or ctrl
            eatCursorUpDown := 
                    (UserPreferences current codeCompletionViewKeyboardNavigationNeedsModifier not)
                    or:[ editView sensor shiftDown 
                    or:[ editView sensor ctrlDown]].

            ((key == #CursorDown and:[eatCursorUpDown])
                or:[ (key == #CursorUp and:[eatCursorUpDown])
                or:[ ((key == #CursorLeft) and:[eatCursorLeftOrRight])
                or:[ ((key == #CursorRight) and:[eatCursorLeftOrRight])
                or:[ ((key == #Return) and:[ completionView hasSelection ])
            ]]]]) ifTrue:[
                "/ forward to completion view
                completionView sensor pushUserEvent:#value for:[ completionView keyPress:key x:0 y:0 ].
                ^ true.
            ].

            (key == #Control_L or:[ key == #Control_R or:[ key == #Control or:[ key == #Ctrl ]]]) ifTrue:[
                "/ CTRL is a toggle
"/                self closeCompletionView.   
"/                ^ true.
                ^ false "/ don't eat
            ].
            (key == #Escape) ifTrue:[
                self closeCompletionView.
                ^ true  "/ EAT
            ].
            "/ shift does not close
            (key == #Shift_L or:[ key == #Shift_R or:[ key == #Shift]]) ifTrue:[
                ^ false "/ don' eat
            ].

            (key == #BackSpace) ifTrue:[
                ^ false "/ don' eat
            ].
            self closeCompletionView.
            ^ false "/ don' eat
        ].
    ].
    ^ false.

    "Modified: / 24-08-2017 / 21:32:52 / cg"
!

postKeyPress:key
    |doComplete|

    UserPreferences current immediateCodeCompletion ifFalse:[
        "/ only update, if already open
        completionView isNil ifTrue:[^ self].
    ].

    (key == #BackSpace or:[key == #BasicBackspace]) ifTrue:[
        autoSelect := false.
        self updateCompletionList.
        ^ self
    ].

    key isCharacter ifTrue:[
        (doComplete := key isSeparator not) ifFalse:[
            "/ also on a separator, but only if at the end of a non-empty line
            doComplete := editView lineStringBeforeCursor withoutSeparators notEmpty.
        ].
        doComplete ifFalse:[
            self closeCompletionView
        ] ifTrue:[
            autoSelect := false.
            self updateCompletionList.
        ].
        ^ self
    ].
!

startTimeoutForEditViewLostFocus
    "see comment in #editViewLostFocus"

    editViewLostFocusBlock isNil ifTrue:[
        editViewLostFocusBlock := [self editViewLostFocus].
    ].
    Processor addTimedBlock:editViewLostFocusBlock afterMilliseconds:200.
! !

!EditTextViewCompletionSupport methodsFor:'private'!

computeCompletions
    "Actually compute the completions and update the completion view."

    self subclassResponsibility

    "Created: / 26-09-2013 / 17:35:17 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

release
    self stopCompletionProcess.
    self closeCompletionView.
    super release
!

startCompletionProcess
    "start the code completion process in the background"

    |initialList cursorX cursorY|

    "/ terminate any previous process
    self stopCompletionProcess.

    (editView sensor hasKeyPressEventFor:nil) ifTrue:[ 
        "/ 'cl' printCR.
        self closeCompletionView. 
        ^ self
    ].
    ((cursorX := editView xOfCursor) isNil
    or:[ (cursorY := editView yOfCursor) isNil ]) ifTrue:[
        "/ no cursor - user is selecting, or cursor has been scrolled out of sight.
        "/ 'cl2' printCR.
        self closeCompletionView. 
        ^ self
    ].

    completionView isNil ifTrue:[
        initialList := #( 'Busy...' ).
        "/ 'op1' printCR.
    ] ifFalse:[
        initialList := completionView list.
        "/ 'op2' printCR.
    ].
    self openCompletionView:initialList.

    completionProcess := 
        [
            "/ protect end-user applications from errors
            Error handle:[:ex |
                Smalltalk isSmalltalkDevelopmentSystem ifTrue:[ ex reject ]
            ] do:[ 
                (editView topView isDebugView) ifTrue:[
                    ControlInterrupt ignoreIn:[
                        self computeCompletions.
                    ].    
                ] ifFalse:[    
                    self computeCompletions.
                ].
            ].
        ] forkAt:(Processor activePriority - 1).

    "Modified: / 26-09-2013 / 17:36:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

stopCompletionProcess
    "kill any background completion process"

    |p|

    (p := completionProcess) notNil ifTrue:[
        completionProcess := nil.
        p terminate.
    ].
!

updateCompletionList
    "called for keypress events"

    self startCompletionProcess.
! !

!EditTextViewCompletionSupport methodsFor:'private-API'!

closeCompletionView
    |v|

    (v := completionView) notNil ifTrue:[
        completionView := nil.
        "/ let it close itself - avoids synchronization problems
        v sensor
            pushUserEvent:#value
            for:[ v topView destroy ]
    ].
!

openCompletionView
    "Opens the completion view with an initial list. Called as soon as
     completion is initiated but completion options are not yet computed."

    self openCompletionView: (Array with: 'Busy...')

    "Created: / 26-09-2013 / 17:06:12 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

openCompletionView: list
    "Makes sure the completion view is opened and with given `list`."
    
    | textCursorPosInTextView textCursorPosOnScreen movePos topView 
      screenBounds screenBoundsCorner 
      helpViewsExtent helpViewsWidth helpViewsHeight|

    "/ move the window away from the text cursor (to not cover what user types in)
    "/ get the screen-relative position of the text cursor
    textCursorPosInTextView := editView xOfCursor @ editView yOfCursor.
    
    "/ care for the scroll-offset (xOfCursor/yOFCursor gives me               
    textCursorPosInTextView := textCursorPosInTextView - (editView viewOrigin x @ 0).
    
    textCursorPosOnScreen := editView device 
                    translatePoint:textCursorPosInTextView 
                    fromView:editView toView:nil.

    "/ currently, we have to stay away a bit, to avoid getting the focus
    "/ this will be somewhat to the down-right of the textCursor
    movePos := textCursorPosOnScreen + (60 @ (editView font height)).

    completionView isNil ifTrue:[
        completionView := CodeCompletionHelpMenuView new.
        completionView name:'completion'.
        completionView level:0.
        completionView list:list.
        completionView enable:false.
        completionView extent:completionView preferredExtentForContents.
        "/ completionView font: editView font.
        topView := CodeCompletionHelpView with:completionView.
        topView editView:editView.
    ] ifFalse:[
        completionView list:list.
        topView := completionView topView.
    ].
    
    topView ~~ completionView ifTrue:[
        topView resizeToFit.

        "/ make sure, the window is visible
        screenBounds := topView device monitorBoundsAt:topView origin.
        screenBoundsCorner := screenBounds corner.

        helpViewsExtent := topView extent.
        helpViewsWidth := helpViewsExtent x.
        helpViewsHeight := helpViewsExtent y.

        "/ if it does not lie completely inside the screen, move it     
        (movePos x + helpViewsWidth) > screenBoundsCorner x ifTrue:[
            movePos := (textCursorPosOnScreen x - 60 - helpViewsWidth) @ movePos y.
        ].
        (movePos y + helpViewsHeight) > screenBoundsCorner y ifTrue:[
            movePos := movePos x @ (textCursorPosOnScreen y - helpViewsHeight).
        ].
        movePos y < 0 ifTrue:[
            movePos := movePos x @ 0
        ].    
        topView origin:movePos.
    ].

    "Created: / 26-09-2013 / 17:07:34 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !


!EditTextViewCompletionSupport class methodsFor:'documentation'!

version
    ^ '$Header$'
!

version_CVS
    ^ '$Header$'
! !