EditFieldWithCompletion.st
author Jan Vrany <jan.vrany@labware.com>
Sat, 30 Sep 2023 22:55:25 +0100
branchjv
changeset 19648 5df52d354504
parent 18226 346376844040
permissions -rw-r--r--
`TestRunner2`: do not use `#keysAndValuesCollect:` ...as semantics differ among smalltalk dialects. This is normally not a problem until we use code that adds this as a "compatibility" method. So to stay on a safe side, avoid using this method.

"
 COPYRIGHT (c) 2006 by eXept Software AG
 COPYRIGHT (c) 2016 Jan Vrany
	      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.
"
"{ Package: 'stx:libtool' }"

"{ NameSpace: Smalltalk }"

EditField subclass:#EditFieldWithCompletion
	instanceVariableNames:'showOptions optionsHolder optionsView optionsWindow completionJob
		tabCompletionJob'
	classVariableNames:''
	poolDictionaries:''
	category:'Views-Text'
!

!EditFieldWithCompletion class methodsFor:'documentation'!

copyright
"
 COPYRIGHT (c) 2006 by eXept Software AG
 COPYRIGHT (c) 2016 Jan Vrany
	      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
"
    an editfield, which does some completion on the Tab key.

    [author:]
        Jan Vrany (janfrog@bruxa)

    [instance variables:]

    [class variables:]

    [see also:]

"
!

example_1
    |top field label value|

    top := StandardSystemView new.
    top
        extent:300 @ 300;
        label:'Live class completion field'.
    value := '' asValue.
    field := self new
                origin:10 @ 135 corner:280 @ 165;
                entryCompletionBlock:[:content | Smalltalk classnameCompletion:content ];
                model:value.
    label := Label new
                origin:10 @ 95 corner:280 @ 115;
                labelChannel:value.
    top
        add:field;
        add:label;
        open
!

examples
"
    Opens a LiveCompletionEditField on a class name
                                                        [exBegin]
    LiveCompletionEditField example_1
                                                        [exEnd]
"
! !

!EditFieldWithCompletion methodsFor:'accepting'!

accept

    self unselect.
    super accept.

    "Created: / 27-07-2009 / 09:38:45 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 12-02-2010 / 11:56:33 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!EditFieldWithCompletion methodsFor:'accessing-behavior'!

showOptions
    ^ showOptions
!

showOptions:aBoolean
    showOptions := aBoolean.
! !

!EditFieldWithCompletion methodsFor:'accessing-dimensions'!

absoluteLeft

    | absoluteLeft view |
    absoluteLeft := 1.
    view := self.
    [ view notNil ] whileTrue:
        [absoluteLeft := absoluteLeft + view left - 1.
        view := view superView].
    ^absoluteLeft

    "Created: / 08-08-2009 / 22:30:07 / Jan Vrany <vranyj1@fel.cvut.cz>"
!

absoluteTop

    | absoluteTop view |
    absoluteTop := 1.
    view := self.
    [ view notNil ] whileTrue:
        [absoluteTop := absoluteTop + view top - 1.
        view := view superView].
    ^absoluteTop

    "Created: / 08-08-2009 / 22:30:16 / Jan Vrany <vranyj1@fel.cvut.cz>"
! !

!EditFieldWithCompletion methodsFor:'accessing-mvc'!

optionsHolder

    ^ optionsHolder

    "Created: / 09-08-2009 / 08:14:02 / Jan Vrany <vranyj1@fel.cvut.cz>"
!

optionsHolder:aValueHolder
    "set the 'options' value holder (automatically generated)"

    optionsHolder := aValueHolder.

    "Created: / 09-08-2009 / 08:14:24 / Jan Vrany <vranyj1@fel.cvut.cz>"
! !

!EditFieldWithCompletion methodsFor:'event handling'!

completion:best options:options 
    |newContent oldContent|
    "
    oldContent := ((self contents ? '') asString).
    newContent := ((best isNil or:[ best = oldContent ]) 
                and:[ options isNilOrEmptyCollection not ]) 
                    ifTrue:[
                        options first
                        ""options inject:options anyOne
                            into:[:shortest :each | 
                                shortest asString size > each asString size ifTrue:[
                                    each
                                ] ifFalse:[ shortest ]
                            ]""
                    ]
                    ifFalse:[ best ].
    self contents:newContent asString.
    self cursorCol:oldContent size + 1.
    oldContent size < newContent size ifTrue:[
        self 
            selectFromLine:1
            col:oldContent size + 1
            toLine:1
            col:newContent size
    ].
    "
    true "options size > 1" ifTrue:[
        optionsHolder value:options.
        self showOptionsWindow
    ]

    "Created: / 08-08-2009 / 22:02:29 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 08:16:41 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 13-02-2010 / 09:51:11 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

handleNonCommandKey: char
    super handleNonCommandKey: char.
    self startCompletion

    "Modified: / 26-07-2009 / 17:41:22 / Jan Vrany <vranyj1@fel.cvut.cz>"
!

handlesKeyPress:key inView:aView
    <resource: #keyboard (#CursorDown #CursorUp)>

    ^ aView == optionsView and:[(#(CursorDown CursorUp) includes:key) not].

    "Created: / 09-12-2010 / 21:31:51 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

hasKeyboardFocus:gotFocusBoolean
    hasKeyboardFocus == gotFocusBoolean ifTrue:[
        "no change"
        ^ self
    ].

    gotFocusBoolean ifTrue:[
        "position cursor behind of already typed chars"
        self selectAll.
    ] ifFalse:[
        (self hasFocus not and:[optionsWindow notNil and:[optionsWindow hasFocus not]]) ifTrue:[
            self unselect.
            self hideOptionsWindow.
        ].
    ].
    ^ super hasKeyboardFocus:gotFocusBoolean

    "Created: / 08-08-2009 / 23:28:50 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 10:14:25 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-12-2010 / 22:05:28 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

keyPress:key x:x y:y 
    "Forward certain events to optionsView if any"

    <resource: #keyboard (#CursorDown #CursorUp #Accept #Return #Escape #BackSpace #Delete)>

    (key isCharacter or:[#(Paste Copy Cut) includesIdentical:key]) ifTrue:[
        super keyPress:key x:x y:y.
        ^ self.
    ].

    (optionsView notNil 
     and:[#(CursorDown CursorUp) includesIdentical:key]) ifTrue:[
        optionsView keyPress:key x:x y:y.
        ^ self.
    ].

    key == #Accept ifTrue:[self hideOptionsWindow; accept].
    key == #Return ifTrue:[self hideOptionsWindow; accept].
    key == #Escape ifTrue:[self hideOptionsWindow].
    (key == #BackSpace or:[key == #Delete or:[key == #Tab]]) ifTrue:[
        super keyPress:key x:x y:y.
        key == #Tab ifTrue:[
            self startTabCompletion
        ] ifFalse:[    
            self startCompletion.
        ].
        ^ self.
    ].


    "Created: / 08-08-2009 / 22:02:13 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 14:06:43 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-02-2010 / 20:47:59 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

keyPress:key x:x y:y view:aView
    self keyPress: key x:x y:y

    "Created: / 09-12-2010 / 21:32:20 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

tabCompletion:best options:options 
    self updateContentsForLongest:best options:options.
    
    true "options size > 1" ifTrue:[
        optionsHolder value:options.
        self showOptionsWindow
    ].
    "/ start another job, to update the list
    self doCompletion.
!

updateContentsForLongest:best options:options 
    |newContent oldContent|
    
    oldContent := ((self contents ? '') asString).
    newContent := best ? oldContent.
    newContent ~= oldContent ifTrue:[
        self contents:newContent asString.
        self cursorCol:oldContent size + 1.
        oldContent size < newContent size ifTrue:[
            self 
                selectFromLine:1 col:oldContent size + 1
                toLine:1 col:newContent size.
            typeOfSelection := #paste
        ].
    ].
! !

!EditFieldWithCompletion methodsFor:'initialization & release'!

destroy
    self hideOptionsWindow.
    super destroy.

    "Created: / 08-08-2009 / 22:16:17 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 08:50:04 / Jan Vrany <vranyj1@fel.cvut.cz>"
!

initialize
    super initialize.
    showOptions := true.
    optionsHolder := ValueHolder new.
    
    completionJob := BackgroundJob 
                        named: 'Edit Field Completion Job'
                        on:[self doCompletion].
    tabCompletionJob := BackgroundJob 
                        named: 'Edit Field Tab Completion Job'
                        on:[self doTabCompletion].

    "Created: / 08-08-2009 / 20:24:05 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 08:17:09 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 03-08-2011 / 17:50:15 / Jan Vrany <jan.vrany@fit.cvut.cz>"
! !

!EditFieldWithCompletion methodsFor:'private'!

doCompletion
    self doCompletionThenSend:#completion:options:
!

doCompletionThenSend:selector
    |oldContents completionInfo options best|

    oldContents := (self contents ? '') asString.
    completionInfo := self entryCompletionBlock 
                    valueWithOptionalArgument:oldContents
                    and:self.
    completionInfo isNil ifTrue:[
        ^ self
    ].
    
    best := completionInfo first.
    options := completionInfo second.
"/    best isNil ifTrue:[self halt ].
    (options includes:best) ifFalse:[
        best := options 
                    detect:[:e | e asString startsWith:best asString ]
                    ifNone:[ best ]
    ].
    options isSortedCollection ifFalse:[
        options := options asSortedCollection:[:a :b | a displayString < b displayString ]
    ].
    self sensor 
        pushUserEvent:selector
        for:self
        withArguments:(Array with:best with:options).

    "Created: / 26-07-2009 / 17:45:09 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 02:51:44 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 18-11-2011 / 14:33:56 / cg"
    "Modified (format): / 20-11-2011 / 09:42:25 / cg"
    "Modified: / 25-07-2016 / 07:55:26 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

doTabCompletion
    self doCompletionThenSend:#tabCompletion:options:
!

hideOptionsWindow
    optionsWindow notNil ifTrue:[
        optionsWindow destroy.
        optionsView := optionsWindow := nil.
    ]

    "Created: / 08-08-2009 / 23:23:45 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 09:00:00 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 29-11-2011 / 11:27:03 / cg"
!

showOptionsWindow
    | x y w |

    showOptions ifFalse:[^ self].
    optionsWindow notNil ifTrue:[ 
        optionsWindow raise.
        ^ self 
    ].

    optionsView := SelectionInListModelView new
                    textStartLeft: textStartLeft - 2;
                    listHolder: optionsHolder;
                    action:[:value | value notNil ifTrue:[self contents:value asString]];
                    doubleClickAction:[:index | 
                            self contents:(optionsView at:index) asString.
                            self hideOptionsWindow.
                            self accept
                        ];
                    useIndex: false;
                    " JV: Looks good to me "
                    highlightMode: #line;
                    font:self font;
                    backgroundColor:self backgroundColor;
                    delegate: self;
                    yourself.

    x := self absoluteLeft + 5" - optionsView textStartLeft".
    y := self absoluteTop + self height + 1 + 5.
    w := (width * 2) + 0"((optionsView textStartLeft) * 2)".

    optionsWindow := StandardSystemView new
        bePopUpView;
        beSlave;        
        origin:x @ y
        extent:(w min: ((Screen current monitorBoundsAt:(x@y)) corner x - x)) @ (fontHeight * 10);
        yourself.

    ScrollableView   
        forView:optionsView 
        hasHorizontalScrollBar:false 
        hasVerticalScrollBar:true 
        miniScrollerH:true 
        miniScrollerV:false 
        origin:0.0@0.0 
        corner:1.0@1.0 
        in:optionsWindow.

    optionsWindow open.

    "Created: / 09-08-2009 / 08:12:21 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 09-08-2009 / 09:28:47 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified: / 29-11-2011 / 11:27:13 / cg"
    "Modified: / 04-04-2012 / 13:08:45 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

startCompletion

    completionJob restart.

    "Created: / 26-07-2009 / 17:41:41 / Jan Vrany <vranyj1@fel.cvut.cz>"
    "Modified (format): / 03-08-2011 / 17:50:53 / Jan Vrany <jan.vrany@fit.cvut.cz>"
!

startTabCompletion

    tabCompletionJob restart.
! !

!EditFieldWithCompletion class methodsFor:'documentation'!

version_CVS
    ^ '$Header$'
!

version_HG

    ^ '$Changeset: <not expanded> $'
!

version_SVN
    ^ '$Id$'
! !