Just Another AutoComplete for string gadgets
Posted: Thu Jul 22, 2010 6:47 am
Update July 26
When I came up with the idea of tapping SQLite for an easy search of text matches, it didn't occur to me that there might be a penalty. In fact, there was. The penalty came in the form of 405 kb being added to the executable size, just for autocompletes. So I looked at the other option I had considered, that of the linked list. Because this is an OOP-interface object, I remember rejecting the linked list on account of not being able to pass a list in an interface. Also, the last thing I wanted was to set up global lists for the gadgets. That's just ugly imho. But wait wait wait! With 4.50+ you can put a list inside a structure! The structure can be passed in an interface. So - SQLite is out, the database in the object structure is replaced with a linked list and all works fine- sans 405 thousand extra bytes of bloat.
Update July 24
Fixed a bug where if you used AutoAdditions too many words would get added:
e.g. new word: Experience
result:
Exp
Expe
Exper
Experi
Experie
Experien
Experienc
Experience
Ack. Anyway, fixed. This is the last bug I can find for now after hours of trying with varying numbers of instances active. I think it's getting close touch wood.
Update July 23
- This update brings a couple of new features:
1) You can navigate from the string gadget to and throughout the list and back again with the arrow keys, select w/space or tab (thanks dige)
2) You can delete an item from the list for a given gadget now with a rightclick (thanks rsts)
- Fixed a bug in the mousehook handling where under certain circumstances the listbox would stay up when it shouldn't
Update July 21
- The mousehook is reworked, it no longer needs shared vars and it can distinguish between multiple instances now. Also, there is only one mouse hook regardless of the number of instances of the object in use.
- Auto-dynamic additions are tweaked to accept a minimum-length in order to qualify for addition to the popup list. The current default is 3 but you can put anything in there that suits you, of course.
- Multiple instances are so far tested working, but we need more keybangers banging away to find any bugs. The current test prog uses 2.
- Thanks to everyone helping to get this project to a solid release, it's appreciated!
Original Post July 21
And a test program:
When I came up with the idea of tapping SQLite for an easy search of text matches, it didn't occur to me that there might be a penalty. In fact, there was. The penalty came in the form of 405 kb being added to the executable size, just for autocompletes. So I looked at the other option I had considered, that of the linked list. Because this is an OOP-interface object, I remember rejecting the linked list on account of not being able to pass a list in an interface. Also, the last thing I wanted was to set up global lists for the gadgets. That's just ugly imho. But wait wait wait! With 4.50+ you can put a list inside a structure! The structure can be passed in an interface. So - SQLite is out, the database in the object structure is replaced with a linked list and all works fine- sans 405 thousand extra bytes of bloat.
Update July 24
Fixed a bug where if you used AutoAdditions too many words would get added:
e.g. new word: Experience
result:
Exp
Expe
Exper
Experi
Experie
Experien
Experienc
Experience
Ack. Anyway, fixed. This is the last bug I can find for now after hours of trying with varying numbers of instances active. I think it's getting close touch wood.
Update July 23
- This update brings a couple of new features:
1) You can navigate from the string gadget to and throughout the list and back again with the arrow keys, select w/space or tab (thanks dige)
2) You can delete an item from the list for a given gadget now with a rightclick (thanks rsts)
- Fixed a bug in the mousehook handling where under certain circumstances the listbox would stay up when it shouldn't
Update July 21
- The mousehook is reworked, it no longer needs shared vars and it can distinguish between multiple instances now. Also, there is only one mouse hook regardless of the number of instances of the object in use.
- Auto-dynamic additions are tweaked to accept a minimum-length in order to qualify for addition to the popup list. The current default is 3 but you can put anything in there that suits you, of course.
- Multiple instances are so far tested working, but we need more keybangers banging away to find any bugs. The current test prog uses 2.
- Thanks to everyone helping to get this project to a solid release, it's appreciated!
Original Post July 21
Code: Select all
;==================================================================
;
; Library: AutoComplete v. 0.9 alpha (WIP)
; Author: Lloyd Gallant (netmaestro)
; Date: July 21, 2010
; Target OS: Microsoft Windows All
; Target Compiler: PureBasic 4.50 and later
; license: Free, unrestricted, no warranty whatsoever
; credit appreciated but not required
;
; Contributors: rsts ( DeleteString )
; dige ( keyboard navigation )
;
;==================================================================
Import ""
PB_Gadget_GetRootWindow(hwnd) ; Find root window of gadgetid
EndImport
Structure MSLLHOOKSTRUCT
pt.point
mouseData.l
flags.l
time.l
dwExtraInfo.l
EndStructure
Structure stringdb
gadget.i
prompt.s
EndStructure
Structure AutoCompleteObject
*vTable
List pstr.stringdb() ; db for strings
subclass_oldproc.i ; oldproc for string gadget (to set back on release)
gadget.i ; string gadget#
popwin.i ; popup window# for list
listbox.i ; gadget# for popup listview
listbox_oldproc.i ; oldproc for listbox
parent.i ; root window of application
dynamic.b ; bool for auto-add content to list on stringgadget lostfocus, 1=add, 0=no add
minlength.b ; minimum number of chars for text to be added to popup list
EndStructure
Interface iAutoCompleteObject
Attach ( gadget, *strings, size )
AddString ( gadget, *string )
DeleteString ( gadget, *string )
Release ()
EndInterface
Global _nmAC_Hook, _nmAC_ListFont = LoadFont(#PB_Any, "MS San Serif", 8)
Global NewList _nmAC_Pops()
Procedure AddString(*this.AutoCompleteObject, gadget, *string )
text$ = PeekS(*string)
ForEach *this\pstr()
If UCase(*this\pstr()\prompt) = UCase(text$)
ProcedureReturn 0
EndIf
Next
AddElement(*this\pstr())
*this\pstr()\gadget = gadget
*this\pstr()\prompt = PeekS(*string)
SortStructuredList(*this\pstr(), #PB_Sort_Ascending, OffsetOf(stringdb\prompt), #PB_Sort_String)
ProcedureReturn 1
EndProcedure
Procedure ShowPopupList(hwnd, *this.AutoCompleteObject)
Protected NewList strings.s()
text$ = GetGadgetText(*this\gadget)
If text$ <> ""
ForEach *this\pstr()
If Left(UCase(*this\pstr()\prompt), Len(text$)) = UCase(text$)
AddElement(strings())
strings()= *this\pstr()\prompt
EndIf
Next
If ListSize(strings())
GetWindowRect_(hwnd, @wr.RECT)
parent = PB_Gadget_GetRootWindow(hwnd)
With wp.point
\x = GadgetX(*this\gadget)
\y = GadgetY(*this\gadget)+GadgetHeight(*this\gadget)
EndWith
ClientToScreen_(parent, @wp)
hdc = CreateCompatibleDC_(0)
lastwidth = 0
ForEach strings()
GetTextExtentPoint32_(hdc, strings(), Len(strings()), @sz.size)
If sz\cx > lastwidth
lastwidth = sz\cx
EndIf
Next
DeleteDC_(hdc)
popwinheight = 15*ListSize(strings())+3
If popwinheight > 500
popwinheight = 500
EndIf
ResizeWindow(*this\popwin, wp\x,wp\y,lastwidth, popwinheight)
ResizeGadget(*this\listbox, 0,0, WindowWidth(*this\popwin),WindowHeight(*this\popwin))
ClearGadgetItems(*this\listbox)
ForEach strings()
AddGadgetItem(*this\listbox, -1, strings())
Next
HideWindow(*this\popwin,0)
SetFocus_(hwnd)
Else
ClearList(strings())
ClearGadgetItems(*this\listbox)
If IsWindowVisible_(WindowID(*this\popwin))
HideWindow(*this\popwin, 1)
EndIf
EndIf
Else
ClearList(strings())
ClearGadgetItems(*this\listbox)
If IsWindowVisible_(WindowID(*this\popwin))
HideWindow(*this\popwin, 1)
EndIf
EndIf
EndProcedure
Procedure _nmAC_StringProc(hwnd, msg, wparam, lparam)
*this.AutoCompleteObject = GetProp_(hwnd, "acdata")
Protected oldproc = *this\subclass_oldproc
Select msg
Case #WM_NCDESTROY
RemoveProp_(hwnd, "oldproc")
Case #WM_LBUTTONUP, #WM_KEYUP
ForEach _nmAC_Pops()
HideWindow(_nmAC_Pops(), 1)
Next
ShowPopupList(hwnd, *this)
If msg=#WM_KEYUP
Select wParam
Case #VK_DOWN
If IsWindowVisible_(WindowID(*this\popwin))
SetActiveGadget(*this\listbox)
SetGadgetState(*this\listbox,0)
EndIf
Case #VK_ESCAPE
ForEach _nmAC_Pops()
HideWindow(_nmAC_Pops(), 1)
Next
EndSelect
EndIf
Case #WM_KILLFOCUS
If *this\dynamic
If wParam <> WindowID(*this\popwin) And wParam <> hwnd
text$ = GetGadgetText(*this\gadget)
If Len(text$)>=*this\minlength
AddString(*this, *this\gadget, @text$)
EndIf
EndIf
EndIf
EndSelect
ProcedureReturn CallWindowProc_(oldproc, hwnd, msg, wparam, lparam)
EndProcedure
Procedure DeleteString(*this.AutoCompleteObject, gadget, *string )
text$ = PeekS(*string)
If text$
ForEach *this\pstr()
If UCase(*this\pstr()\prompt) = UCase(text$)
DeleteElement(*this\pstr())
ProcedureReturn 1
EndIf
Next
ProcedureReturn 0
Else
ProcedureReturn 0
EndIf
EndProcedure
Procedure _nmAC_ListProc(hwnd, msg, wparam, lparam)
Static hold=1 ; Logical device to prevent premature jump to String Gadget
*this.AutoCompleteObject = GetProp_(hwnd, "acdata")
oldproc = *this\listbox_oldproc
Select msg
Case #WM_NCDESTROY
RemoveProp_(hwnd, "oldproc")
Case #WM_KEYUP
If wParam = #VK_TAB Or wParam = #VK_SPACE Or wparam = #VK_RETURN
text$ = GetGadgetText(*this\listbox)
SetGadgetText(*this\gadget, text$)
HideWindow(*this\popwin, 1)
ElseIf wparam = #VK_ESCAPE
HideWindow(*this\popwin, 1)
ElseIf wparam = #VK_UP
If GetGadgetState(*this\listbox) = 0
If Not hold
SetGadgetState(*this\listbox, -1)
SetActiveGadget(*this\gadget)
EndIf
EndIf
EndIf
Case #WM_KEYDOWN
If GetGadgetState(*this\listbox) = 0
hold=0
Else
hold=1
EndIf
Case #WM_LBUTTONDBLCLK
text$ = GetGadgetText(*this\listbox)
SetGadgetText(*this\gadget, text$)
HideWindow(*this\popwin, 1)
Case #WM_RBUTTONUP
text$ = GetGadgetText(*this\listbox)
If text$
If MessageRequester("Autocomplete:","Remove "+Chr(34)+text$+Chr(34)+" from this List?", #MB_YESNO) = #PB_MessageRequester_Yes
Deletestring(*this, *this\gadget, @text$)
EndIf
EndIf
SetFocus_(*this\gadget)
ShowPopupList(GadgetID(*this\gadget), *this)
EndSelect
ProcedureReturn CallWindowProc_(oldproc, hwnd, msg, wparam, lparam)
EndProcedure
Procedure MouseHook(nCode, wParam, lParam)
If wParam = #WM_LBUTTONDOWN
*ms.MSLLHOOKSTRUCT = lparam
clickwindow = WindowFromPoint_(*ms\pt\x|(*ms\pt\y<<32))
If Not GetProp_(clickwindow, "acdata")
wn$ = Space(100)
GetWindowText_(clickwindow, @wn$, 99)
If wn$ <> "Autocomplete:" And wn$ <> "&Yes" And wn$<> "&No"
ForEach _nmAC_Pops()
HideWindow(_nmAC_Pops(), 1)
Next
EndIf
EndIf
EndIf
ProcedureReturn CallNextHookEx_(_nmAC_Hook, nCode, wParam, lParam)
EndProcedure
Procedure Attach(*this.AutoCompleteObject, gadget, *strings, size )
Protected fail_status = #False
*ptr = *strings
For i=1 To size
AddElement(*this\pstr())
*this\pstr()\gadget = gadget
*this\pstr()\prompt = PeekS(PeekL(*ptr))
*ptr+SizeOf(integer)
Next
SortStructuredList(*this\pstr(), #PB_Sort_Ascending, OffsetOf(stringdb\prompt), #PB_Sort_String)
If IsGadget(gadget) And GadgetType(gadget) = #PB_GadgetType_String
*this\gadget = gadget
SetProp_(GadgetID(gadget), "acdata", *this )
*this\subclass_oldproc = SetWindowLongPtr_(GadgetID(gadget), #GWL_WNDPROC, @_nmAC_StringProc())
*this\parent = PB_Gadget_GetRootWindow(hwnd)
*this\popwin = OpenWindow(#PB_Any, 0,0,200,100,"ACPopup",#PB_Window_BorderLess|#PB_Window_Invisible, *this\parent)
AddElement(_nmAC_Pops())
_nmAC_Pops() = *this\popwin
SetWindowLongPtr_(WindowID(*this\popwin), #GWL_EXSTYLE, GetWindowLongPtr_(WindowID(*this\popwin), #GWL_EXSTYLE)|#WS_EX_TOOLWINDOW|#WS_EX_STATICEDGE)
SetWindowPos_(WindowID(*this\popwin), 0,0,0,0,0,#SWP_NOMOVE|#SWP_NOSIZE|#SWP_NOZORDER|#SWP_FRAMECHANGED)
StickyWindow(*this\popwin, 1)
old = UseGadgetList(0)
*this\listbox = ListViewGadget(#PB_Any,0,0,200,100)
UseGadgetList(old)
SetGadgetFont(*this\listbox, FontID(_nmAC_ListFont))
SetProp_(GadgetID(*this\listbox), "acdata", *this )
*this\listbox_oldproc = SetWindowLongPtr_(GadgetID(*this\listbox), #GWL_WNDPROC, @_nmAC_ListProc())
If Not _nmAC_Hook
_nmAC_Hook = SetWindowsHookEx_(#WH_MOUSE_LL, @MouseHook(), 0, 0)
EndIf
Else
fail_status = #True
EndIf
ProcedureReturn 1-fail_status ; 1=success, 0=failed
EndProcedure
Procedure NewObject_Autocomplete(dynamic=0, minlength=3) ; ( [dynamic], [minlength] )
*newobject.AutoCompleteObject = AllocateMemory(SizeOf(AutoCompleteObject))
If *newobject
InitializeStructure(*newobject, AutoCompleteObject)
With *newobject
\vTable = ?AutoComplete_Methods
\dynamic = dynamic
\minlength = minlength
EndWith
ProcedureReturn *newobject
Else
ProcedureReturn 0
EndIf
EndProcedure
Procedure Release(*this.AutoCompleteObject)
If *this\subclass_oldproc
SetWindowLongPtr_(GadgetID(*this\gadget),#GWL_WNDPROC, *this\subclass_oldproc)
EndIf
If IsWindow(*this\popwin)
CloseWindow(*this\popwin)
EndIf
ClearStructure(*this, AutoCompleteObject)
FreeMemory(*this)
EndProcedure
DataSection
AutoComplete_Methods:
Data.l @Attach(), @AddString(), @DeleteString(), @Release()
EndDataSection
;=================================================================
; END OF INCLUDE CODESECTION
;=================================================================
Code: Select all
XIncludeFile "Autocomplete_Object.pbi"
Dim Strings.s(17)
Strings(0) = "Else"
Strings(1) = "ElseIf"
Strings(2) = "EnableDebugger"
Strings(3) = "EnableExplicit"
Strings(4) = "End"
Strings(5) = "EndDataSection"
Strings(6) = "EndEnumeration"
Strings(7) = "EndIf"
Strings(8) = "EndImport"
Strings(9) = "EndInterface"
Strings(10) = "EndMacro"
Strings(11) = "EndProcedure"
Strings(12) = "EndSelect"
Strings(13) = "EndStructure"
Strings(14) = "EndStructureUnion"
Strings(15) = "EndWith"
Strings(16) = "Enumeration"
w=OpenWindow(#PB_Any,0,0,640,480,"",#PB_Window_ScreenCentered|#PB_Window_SystemMenu)
StringGadget(6, 10,10, 300,20,"")
StringGadget(7, 10,50, 300,20,"")
prompts.iAutoCompleteObject = NewObject_Autocomplete(1)
prompts\Attach(6, @strings(), 17 )
prompts\AddString(6, @"Excellent")
prompts2.iAutoCompleteObject = NewObject_Autocomplete()
prompts2\Attach(7, @strings(), 17 )
SetActiveGadget(6)
Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
prompts\Release()
prompts2\Release()