Autocomplete on StringGadget

Share your advanced PureBasic knowledge/code with the community.
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 372
Joined: Sat Dec 25, 2004 2:37 pm

Autocomplete on StringGadget

Post by thyphoon »

Hello,

I've updated my old code to add autocomplete using a StringAdget. You can try it in the first field by typing @ for a person and # for a computer. The second field is more traditional.

I've tried to make the code cross-platform, but for now, it's only been tested on Windows.

If you see any improvements, please let me know.

Code: Select all

; ******************************************************************** 
; Program:           AutoComplete List
; Description:       Add an AutoComplete list to a String gadget 

; Author:            Thyphoon
; Date:              January, 2019 updated 2026
; License:           Free, unrestricted, credit 
;                    appreciated but not required.
; Note:              Please share improvement !
; Thanks to:         Srod, Kiffi
; ******************************************************************** 

EnableExplicit

DeclareModule AutoComplete
  Declare AddAutocompleteWindow(Window.i,Gadget.i,*CallBack, TriggerChars.s="", FirstEventKey.l=5000)
  Declare CheckEvent(Event.i)
EndDeclareModule  

Module AutoComplete
  Declare MoveWindow()

  Prototype.s CallBack(Trigger.s, String.s)
  
  Structure ac
    Window.i                  ;Original Window
    Gadget.i                  ;Original Gadget
    FirstEventKey.l           ;First Eventkey=Up +1=Down +2=Enter +3=Escape
    CallBack.CallBack         ;CallBack who return choice
    TriggerChars.s            ;Trigger characters for autocomplete
    LastTriggerPos.l          ;Last trigger position (1-based)
    LastRightPos.l            ;Right bound of word (1-based)
    LastTriggerChar.s         ;Last trigger character
  EndStructure
  
  Structure params
    acWindow.i                ;Autocomplete window
    acGadget.i                ;AutoComplete GadgetList
    CurrentName.s             ;CurrentName at last open autocomplete windows
    Map AutoCompleteGadget.ac()
    FirstEventKey.i
  EndStructure
  
  Global params.params
  
  Procedure.i _GetCaretPos(Gadget.i)
    CompilerIf #PB_Compiler_OS = #PB_OS_Windows
      Protected start.i, finish.i
      SendMessage_(GadgetID(Gadget), #EM_GETSEL, @start, @finish)
      ProcedureReturn start
    CompilerElseIf #PB_Compiler_OS = #PB_OS_Linux
      ProcedureReturn gtk_editable_get_position_(GadgetID(Gadget))
    CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS
      Protected nsTextField.i = GadgetID(Gadget)
      Protected editor.i = CocoaMessage(0, nsTextField, "currentEditor")
      If editor
        Protected range.NSRange
        CocoaMessage(@range, editor, "selectedRange")
        ProcedureReturn range\location
      EndIf
      ProcedureReturn 0
    CompilerEndIf

    ProcedureReturn 0
  EndProcedure
  
  Procedure AddAutocompleteWindow(Window.i,Gadget.i,*CallBack, TriggerChars.s="", FirstEventKey.l=5000)
    Name.s=Str(Window)+"-"+Str(Gadget)
    params\AutoCompleteGadget(Name)\CallBack=*CallBack
    params\AutoCompleteGadget(Name)\Gadget=Gadget
    params\AutoCompleteGadget(Name)\Window=Window
    params\AutoCompleteGadget(Name)\FirstEventKey=FirstEventKey
    params\AutoCompleteGadget(Name)\TriggerChars=TriggerChars
    AddKeyboardShortcut(Window, #PB_Shortcut_Up, FirstEventKey)
    AddKeyboardShortcut(Window, #PB_Shortcut_Down, FirstEventKey+1)
    AddKeyboardShortcut(Window, #PB_Shortcut_Return, FirstEventKey+2)
    AddKeyboardShortcut(Window, #PB_Shortcut_Escape, FirstEventKey+3)
  EndProcedure
  
  Procedure RefreshAutocompleteWindow(Name.s)
    Gadget.i=params\AutoCompleteGadget(Name)\Gadget
    X.l=GadgetX(Gadget,#PB_Gadget_ScreenCoordinate)
    Y.l=GadgetY(Gadget,#PB_Gadget_ScreenCoordinate)+GadgetHeight(Gadget)
    W.l=GadgetWidth(Gadget)
    H.l=150
    ResizeWindow(params\acWindow,X,Y,W,H)
    ResizeGadget(params\acGadget,0,0,W,H)
    HideWindow(params\acWindow, #False,#PB_Window_NoActivate)
    StickyWindow(params\acWindow,#True)
  EndProcedure
  
  Procedure UpdateAutocompleteList(Name.s)
    ClearGadgetItems(params\acGadget)
    If params\AutoCompleteGadget(Name)\CallBack<>0
      Protected fullText.s = GetGadgetText(params\AutoCompleteGadget(Name)\Gadget)
      Protected caret.i = _GetCaretPos(params\AutoCompleteGadget(Name)\Gadget)
      Protected word.s = ""
      Protected l.l, r.l, c.s
      Protected triggerPos.l = 0
      Protected triggerChar.s = ""

      If caret < 0 : caret = Len(fullText) : EndIf
      If caret > Len(fullText) : caret = Len(fullText) : EndIf

      If params\AutoCompleteGadget(Name)\TriggerChars <> ""
        For l = caret To 1 Step -1
          c = Mid(fullText, l, 1)
          If c = " "
            Break
          EndIf
          If FindString(params\AutoCompleteGadget(Name)\TriggerChars, c)
            triggerPos = l
            triggerChar = c
            word = Mid(fullText, l + 1, caret - l)
            Break
          EndIf
        Next

        If triggerPos = 0
          HideWindow(params\acWindow, #True)
          ProcedureReturn
        EndIf
      Else
        word = Left(fullText, caret)
      EndIf

      r = caret + 1
      While r <= Len(fullText)
        c = Mid(fullText, r, 1)
        If c = " "
          Break
        EndIf
        r + 1
      Wend

      params\AutoCompleteGadget(Name)\LastTriggerPos = triggerPos
      params\AutoCompleteGadget(Name)\LastRightPos = r
      params\AutoCompleteGadget(Name)\LastTriggerChar = triggerChar

      BackString.s = params\AutoCompleteGadget(Name)\CallBack(word,triggerChar)
      For n=1 To CountString(BackString,Chr(10))+1
        line.s=StringField(BackString,n,Chr(10))
        If word = "" Or LCase(Left(line, Len(word))) = LCase(word)
          AddGadgetItem(params\acGadget,-1,line)
        EndIf 
      Next
      ;Hide Autocomplete Window if no Choice
      If CountGadgetItems(params\acGadget)=0
        HideWindow(params\acWindow,#True)
      Else
        HideWindow(params\acWindow,#False,#PB_Window_NoActivate)
      EndIf 
    Else
      Debug "Error with callback"
    EndIf
  EndProcedure
  
  Procedure OpenAutocompleteWindow(Name.s)
    If params\acGadget>0 And IsGadget(params\acGadget):FreeGadget(params\acGadget):EndIf
    If params\acWindow>0 And IsWindow(params\acWindow):CloseWindow(params\acWindow):EndIf
    params\acWindow=OpenWindow(#PB_Any,0,0,50,50,"AutoComplete",#PB_Window_BorderLess|#PB_Window_NoActivate|#PB_Window_Invisible,WindowID(params\AutoCompleteGadget(Name)\Window));
    If params\acWindow
      params\CurrentName=Name
      SetWindowData(params\acWindow,params\AutoCompleteGadget(Name)\Gadget)
      params\acGadget=ListViewGadget(#PB_Any, 0, 0, 50,50)
    EndIf
    RefreshAutocompleteWindow(Name)
    UpdateAutocompleteList(Name)
    BindEvent(#PB_Event_MoveWindow, @MoveWindow(), params\AutoCompleteGadget(Name)\Window)
  EndProcedure
  
  Procedure CloseAutocompleteWindow(Name.s)
      If params\acGadget>0 And IsGadget(params\acGadget):FreeGadget(params\acGadget):EndIf
      If params\acWindow>0 And IsWindow(params\acWindow):CloseWindow(params\acWindow):EndIf
      UnbindEvent(#PB_Event_MoveWindow, @MoveWindow(), Windows)
  EndProcedure
  
  Procedure MoveWindow()
    If EventWindow()=params\AutoCompleteGadget()\Window And params\acWindow>0 And IsWindow(params\acWindow)
      RefreshAutocompleteWindow(params\CurrentName)
    EndIf
  EndProcedure



  Procedure _ReplaceAfterTrigger(Name.s, NewText.s)
    Protected fullText.s = GetGadgetText(params\AutoCompleteGadget(Name)\Gadget)
    Protected caret.i = _GetCaretPos(params\AutoCompleteGadget(Name)\Gadget)
    Protected l.l, r.l
    Protected c.s
    Protected newCaret.i
    Protected triggerPos.l = params\AutoCompleteGadget(Name)\LastTriggerPos
    Protected rightPos.l = params\AutoCompleteGadget(Name)\LastRightPos

    If caret < 0 : caret = Len(fullText) : EndIf
    If caret > Len(fullText) : caret = Len(fullText) : EndIf

    If rightPos <= 0 Or rightPos > Len(fullText) + 1
      rightPos = Len(fullText) + 1
    EndIf

    Protected suffix.s = Mid(fullText, rightPos)
    If suffix <> "" And Left(suffix, 1) <> " "
      suffix = " " + suffix
      newCaret = triggerPos + Len(NewText) + 1
    Else
      newCaret = triggerPos + Len(NewText)
    EndIf

    If triggerPos > 0
      SetGadgetText(params\AutoCompleteGadget(Name)\Gadget, Left(fullText, triggerPos) + NewText + suffix)
    Else
      SetGadgetText(params\AutoCompleteGadget(Name)\Gadget, NewText + suffix)
      newCaret = Len(NewText)
      If suffix <> "" And Left(suffix, 1) = " "
        newCaret + 1
      EndIf
    EndIf
    CompilerIf #PB_Compiler_OS = #PB_OS_Windows
      SendMessage_(GadgetID(params\AutoCompleteGadget(Name)\Gadget), #EM_SETSEL, newCaret, newCaret)
    CompilerElseIf #PB_Compiler_OS = #PB_OS_Linux
      gtk_editable_set_position_(GadgetID(params\AutoCompleteGadget(Name)\Gadget), newCaret)
    CompilerElseIf #PB_Compiler_OS = #PB_OS_MacOS
      Protected nsTextField.i = GadgetID(params\AutoCompleteGadget(Name)\Gadget)
      Protected editor.i = CocoaMessage(0, nsTextField, "currentEditor")
      If editor
        Protected range.NSRange
        range\location = newCaret
        range\length = 0
        CocoaMessage(0, editor, "setSelectedRange:@", @range)
      EndIf
    CompilerEndIf
  EndProcedure
  

  Procedure CheckEvent(Event.i)
    Static bbclick.b
    ;-Orignal Gadget Event
    Protected Name.s=Str(EventWindow())+"-"+Str(EventGadget())   
    If FindMapElement(params\AutoCompleteGadget(),Name)
      Select Event
        Case  #PB_Event_Gadget
          Select EventGadget()
              ; Main gadget Event  
            Case params\AutoCompleteGadget()\Gadget
              Select EventType()
                Case #PB_EventType_Focus
                  If params\acWindow=0 Or IsWindow(params\acWindow)=#False
                    If bbclick=#False
                      OpenAutocompleteWindow(Name)
                    Else
                      bbclick=#True
                    EndIf 
                  EndIf 
                Case #PB_EventType_LostFocus
                  If GetActiveGadget()<>params\acGadget
                    CloseAutocompleteWindow(Name)
                    bbclick=#False
                  EndIf  
                Case #PB_EventType_Change
                  If bbclick=#False 
                    If IsWindow(params\acWindow)
                      UpdateAutocompleteList(Name)
                    Else
                      Debug "pas de windows"
                    EndIf
                  Else
                    OpenAutocompleteWindow(Name)
                    bbclick=#False
                  EndIf 
              EndSelect 

          EndSelect 
     EndSelect
    EndIf
    
    Select Event
      ;if Keyword  
      Case #PB_Event_Menu
        Select EventMenu()
          Case params\AutoCompleteGadget(params\CurrentName)\FirstEventKey;UP
            SetGadgetState(params\acGadget,GetGadgetState(params\acGadget)-1)
          Case params\AutoCompleteGadget(params\CurrentName)\FirstEventKey+1;Down
            SetGadgetState(params\acGadget,GetGadgetState(params\acGadget)+1)
          Case params\AutoCompleteGadget(params\CurrentName)\FirstEventKey+2;Enter
            _ReplaceAfterTrigger(params\CurrentName, GetGadgetItemText(params\acGadget, GetGadgetState(params\acGadget)))
          Case params\AutoCompleteGadget(params\CurrentName)\FirstEventKey+3;
            CloseAutocompleteWindow(params\CurrentName)
        EndSelect
      Case  #PB_Event_Gadget
        If EventGadget()=params\acGadget
          If EventType()=#PB_EventType_LeftDoubleClick
            _ReplaceAfterTrigger(params\CurrentName, GetGadgetItemText(params\acGadget, GetGadgetState(params\acGadget)))
            CloseAutocompleteWindow(params\CurrentName)
            bbclick=#True
          EndIf
        EndIf 
    EndSelect  
    
    
    
  EndProcedure  
  
  
EndModule

;- MAIN TEST

CompilerIf #PB_Compiler_IsMainFile 
  
  ; Callback to return keyword list. you must use a database en filter if you want ^_^ 
  Procedure.s ToDisplayInList(String.s,Trigger.s)
    If Trigger="#"
      ProcedureReturn "Amiga"+Chr(10)+"Amstrad"+Chr(10)+"Atari"+Chr(10)+"Commodore"+Chr(10)+"BeBox"+Chr(10)+"Macintosh"+Chr(10)+"Spectrum"
    ElseIf Trigger="@"
      ProcedureReturn "Alain"+Chr(10)+"Bernard"+Chr(10)+"Charly"+Chr(10)+"David"+Chr(10)+"Eric"+Chr(10)+"Filipe"+Chr(10)+"Horace"+Chr(10)+"Jérome"+Chr(10)+"Kevin"+Chr(10)+"Laurent"+Chr(10)+"Marc"
    EndIf 
  EndProcedure
  
  Procedure.s ToDisplayInListB(String.s,Trigger.s)
    ProcedureReturn "Alain"+Chr(10)+"Bernard"+Chr(10)+"Charly"+Chr(10)+"David"+Chr(10)+"Eric"+Chr(10)+"Filipe"+Chr(10)+"Horace"+Chr(10)+"Jérome"+Chr(10)+"Kevin"+Chr(10)+"Laurent"+Chr(10)+"Marc"
  EndProcedure
  
  If OpenWindow(0, 200, 200, 800, 100, "AutoComplete Teste", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
    
    StringGadget(1, 10, 10, 380, 26, "")
    StringGadget(2, 410, 10, 380, 26, "")
    
    AutoComplete::AddAutocompleteWindow(0,1,@ToDisplayInList(),"@#*") ;Just init the gadget who must support AutoComplete list
    AutoComplete::AddAutocompleteWindow(0,2,@ToDisplayInListB()," ")
    Repeat
      Define Event.i = WaitWindowEvent()
      
      
      AutoComplete::CheckEvent(Event)
      
      Select Event
        Case #PB_Event_Gadget
      EndSelect
    Until Event=#PB_Event_CloseWindow
  EndIf
  End
CompilerEndIf

Last edited by thyphoon on Mon Feb 02, 2026 5:36 am, edited 3 times in total.
User avatar
Kiffi
Addict
Addict
Posts: 1520
Joined: Tue Mar 02, 2004 1:20 pm
Location: Amphibios 9

Re: Autocomplete on StringGadget

Post by Kiffi »

Line 219 is windows only:

Code: Select all

SendMessage_(GadgetID(params\AutoCompleteGadget(Name)\Gadget), #EM_SETSEL, newCaret, newCaret)
Hygge
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 372
Joined: Sat Dec 25, 2004 2:37 pm

Re: Autocomplete on StringGadget

Post by thyphoon »

Kiffi wrote: Sun Feb 01, 2026 3:36 pm Line 219 is windows only:
Thanks !😅 Bad version copy paste ...I updated first post😜
SMaag
Enthusiast
Enthusiast
Posts: 367
Joined: Sat Jan 14, 2023 6:55 pm
Location: Bavaria/Germany

Re: Autocomplete on StringGadget

Post by SMaag »

under Linux the '_' for gtk_editable_get_position is missiung!
Change to gtk_editable_get_position_
in Line 56, 217
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 372
Joined: Sat Dec 25, 2004 2:37 pm

Re: Autocomplete on StringGadget

Post by thyphoon »

SMaag wrote: Sun Feb 01, 2026 8:32 pm under Linux the '_' for gtk_editable_get_position is missiung!
Change to gtk_editable_get_position_
in Line 56, 217
Thanks 🥰 I updated Code
User avatar
Kiffi
Addict
Addict
Posts: 1520
Joined: Tue Mar 02, 2004 1:20 pm
Location: Amphibios 9

Re: Autocomplete on StringGadget

Post by Kiffi »

MacOS: When the StringGadget(1) receives the Focus for the first time, the Autocomplete window flashes briefly. That doesn't look nice. It looks better for me if I enter #PB_Window_Invisible as an additional flag:

Code: Select all

params\acWindow=OpenWindow(#PB_Any,0,0,50,50,"AutoComplete",#PB_Window_BorderLess|#PB_Window_NoActivate|#PB_Window_Invisible,WindowID(params\AutoCompleteGadget(Name)\Window));
Hygge
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 372
Joined: Sat Dec 25, 2004 2:37 pm

Re: Autocomplete on StringGadget

Post by thyphoon »

Kiffi wrote: Sun Feb 01, 2026 11:05 pm MacOS: When the StringGadget(1) receives the Focus for the first time, the Autocomplete window flashes briefly. That doesn't look nice. It looks better for me if I enter #PB_Window_Invisible as an additional flag:
Good idea ! I take and i update the code on the first post 🥳
User avatar
Michael Vogel
Addict
Addict
Posts: 2847
Joined: Thu Feb 09, 2006 11:27 pm
Contact:

Re: Autocomplete on StringGadget

Post by Michael Vogel »

Found a small issue, I've entered the following keys (after F5 and Tab):

@ J 'down' 'Enter' 'Enter' 'Enter'

then "@Jérome érome érome" in seen in the edit field.
User avatar
thyphoon
Enthusiast
Enthusiast
Posts: 372
Joined: Sat Dec 25, 2004 2:37 pm

Re: Autocomplete on StringGadget

Post by thyphoon »

Michael Vogel wrote: Wed Feb 04, 2026 6:54 pm Found a small issue, I've entered the following keys (after F5 and Tab):

@ J 'down' 'Enter' 'Enter' 'Enter'

then "@Jérome érome érome" in seen in the edit field.
thanks i will search a solution 🤔
Post Reply