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

