Introduction

The TextCtrlAutoComplete control is a control used where rapid data entry is needed. It is akin to the ComboBox except that typing in the TextCtrl opens the drop down list and selects the first item that starts with what you are typing. This control has been implemented to mimic as much as possible Firefox's <input type="text"> boxes. Single clicking in it toggles the drop down menu (with choices that you define). Hitting enter when an item is selected (or clicking the item) selects the item. Escape hides the drop down. Hitting down arrow moves the selection down one item, and conversely with up.

(Note: this code does not currently run on Mac, since wxMAC does not yet support PopupWindow.)

(Will Sadkin 8/05/2008: This may no longer be true-- see below.)

(Daniel Levine 7/10/2010: This is still not supported on Macs as of wxPython 2.8.10.1)

Michele Petrazzo modified the code for use wxListCtrl instead of wxListBox, so now TextCtrlAutoComplete can handle more than one column, has column sorting, can fetch the data on the column you want and at the end, can call a "callback" function when an item are select.

Will Sadkin modified the code to allow dynamic generation of the select list based on what's typed, added an argument to hide the dropdown if no match, added an option to set a match function, and enhanced the demo to show how you can now do dynamic list updates based on what is typed.

For easier downloading, the code is also available via this attachment: TextCtrlAutoComplete.py

Code

   1 '''
   2 wxPython Custom Widget Collection 20060207
   3 Written By: Edward Flick (eddy -=at=- cdf-imaging -=dot=- com)
   4             Michele Petrazzo (michele -=dot=- petrazzo -=at=- unipex -=dot=- it)
   5             Will Sadkin (wsadkin-=at=- nameconnector -=dot=- com)
   6 Copyright 2006 (c) CDF Inc. ( http://www.cdf-imaging.com )
   7 Contributed to the wxPython project under the wxPython project's license.
   8 '''
   9 import locale, wx, sys, cStringIO
  10 import  wx.lib.mixins.listctrl  as  listmix
  11 from wx import ImageFromStream, BitmapFromImage
  12 #----------------------------------------------------------------------
  13 def getSmallUpArrowData():
  14     return \
'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
  15 \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
  16 \x00\x00<IDAT8\x8dcddbf\xa0\x040Q\xa4{h\x18\xf0\xff\xdf\xdf\xffd\x1b\x00\xd3\
  17 \x8c\xcf\x10\x9c\x06\xa0k\xc2e\x08m\xc2\x00\x97m\xd8\xc41\x0c \x14h\xe8\xf2\
  18 \x8c\xa3)q\x10\x18\x00\x00R\xd8#\xec\xb2\xcd\xc1Y\x00\x00\x00\x00IEND\xaeB`\
  19 \x82'
  20 
  21 def getSmallUpArrowBitmap():
  22     return BitmapFromImage(getSmallUpArrowImage())
  23 
  24 def getSmallUpArrowImage():
  25     stream = cStringIO.StringIO(getSmallUpArrowData())
  26     return ImageFromStream(stream)
  27 
  28 def getSmallDnArrowData():
  29     return \
"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
  30 \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
  31 \x00\x00HIDAT8\x8dcddbf\xa0\x040Q\xa4{\xd4\x00\x06\x06\x06\x06\x06\x16t\x81\
  32 \xff\xff\xfe\xfe'\xa4\x89\x91\x89\x99\x11\xa7\x0b\x90%\ti\xc6j\x00>C\xb0\x89\
  33 \xd3.\x10\xd1m\xc3\xe5*\xbc.\x80i\xc2\x17.\x8c\xa3y\x81\x01\x00\xa1\x0e\x04e\
  34 ?\x84B\xef\x00\x00\x00\x00IEND\xaeB`\x82"
  35 
  36 def getSmallDnArrowBitmap():
  37     return BitmapFromImage(getSmallDnArrowImage())
  38 
  39 def getSmallDnArrowImage():
  40     stream = cStringIO.StringIO(getSmallDnArrowData())
  41     return ImageFromStream(stream)
  42 
  43 
  44 #----------------------------------------------------------------------
  45 class myListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
  46     def __init__(self, parent, ID=-1, pos=wx.DefaultPosition,
  47                  size=wx.DefaultSize, style=0):
  48         wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
  49         listmix.ListCtrlAutoWidthMixin.__init__(self)
  50 
  51 class TextCtrlAutoComplete (wx.TextCtrl, listmix.ColumnSorterMixin ):
  52     def __init__ ( self, parent, colNames=None, choices = None,
  53                   multiChoices=None, showHead=True, dropDownClick=True,
  54                   colFetch=-1, colSearch=0, hideOnNoMatch=True,
  55                   selectCallback=None, entryCallback=None, matchFunction=None,
  56                   **therest) :
  57         '''
  58         Constructor works just like wx.TextCtrl except you can pass in a
  59         list of choices.  You can also change the choice list at any time
  60         by calling setChoices.
  61         '''
  62         if 'style' in therest:
  63             therest['style']=wx.TE_PROCESS_ENTER | therest['style']
  64         else:
  65             therest['style']=wx.TE_PROCESS_ENTER
  66         wx.TextCtrl.__init__(self, parent, **therest )
  67         #Some variables
  68         self._dropDownClick = dropDownClick
  69         self._colNames = colNames
  70         self._multiChoices = multiChoices
  71         self._showHead = showHead
  72         self._choices = choices
  73         self._lastinsertionpoint = 0
  74         self._hideOnNoMatch = hideOnNoMatch
  75         self._selectCallback = selectCallback
  76         self._entryCallback = entryCallback
  77         self._matchFunction = matchFunction
  78         self._screenheight = wx.SystemSettings.GetMetric( wx.SYS_SCREEN_Y )
  79         #sort variable needed by listmix
  80         self.itemDataMap = dict()
  81         #Load and sort data
  82         if not (self._multiChoices or self._choices):
  83             raise ValueError, "Pass me at least one of multiChoices OR choices"
  84         #widgets
  85         self.dropdown = wx.PopupWindow( self )
  86         #Control the style
  87         flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
  88         if not (showHead and multiChoices) :
  89             flags = flags | wx.LC_NO_HEADER
  90         #Create the list and bind the events
  91         self.dropdownlistbox = myListCtrl( self.dropdown, style=flags,
  92                                  pos=wx.Point( 0, 0) )
  93         #initialize the parent
  94         if multiChoices: ln = len(multiChoices)
  95         else: ln = 1
  96         #else: ln = len(choices)
  97         listmix.ColumnSorterMixin.__init__(self, ln)
  98         #load the data
  99         if multiChoices: self.SetMultipleChoices (multiChoices, colSearch=colSearch, colFetch=colFetch)
 100         else: self.SetChoices ( choices )
 101         gp = self
 102         while gp != None :
 103             gp.Bind ( wx.EVT_MOVE , self.onControlChanged, gp )
 104             gp.Bind ( wx.EVT_SIZE , self.onControlChanged, gp )
 105             gp = gp.GetParent()
 106         self.Bind( wx.EVT_KILL_FOCUS, self.onControlChanged, self )
 107         self.Bind( wx.EVT_TEXT , self.onEnteredText, self )
 108         self.Bind( wx.EVT_KEY_DOWN , self.onKeyDown, self )
 109         #If need drop down on left click
 110         if dropDownClick:
 111             self.Bind ( wx.EVT_LEFT_DOWN , self.onClickToggleDown, self )
 112             self.Bind ( wx.EVT_LEFT_UP , self.onClickToggleUp, self )
 113         self.dropdown.Bind( wx.EVT_LISTBOX , self.onListItemSelected, self.dropdownlistbox )
 114         self.dropdownlistbox.Bind(wx.EVT_LEFT_DOWN, self.onListClick)
 115         self.dropdownlistbox.Bind(wx.EVT_LEFT_DCLICK, self.onListDClick)
 116         self.dropdownlistbox.Bind(wx.EVT_LIST_COL_CLICK, self.onListColClick)
 117         self.il = wx.ImageList(16, 16)
 118         self.sm_dn = self.il.Add(getSmallDnArrowBitmap())
 119         self.sm_up = self.il.Add(getSmallUpArrowBitmap())
 120         self.dropdownlistbox.SetImageList(self.il, wx.IMAGE_LIST_SMALL)
 121         self._ascending = True
 122 
 123     #-- methods called from mixin class
 124     def GetSortImages(self):
 125         return (self.sm_dn, self.sm_up)
 126 
 127     def GetListCtrl(self):
 128         return self.dropdownlistbox
 129     # -- event methods
 130 
 131     def onListClick(self, evt):
 132         toSel, flag = self.dropdownlistbox.HitTest( evt.GetPosition() )
 133         #no values on poition, return
 134         if toSel == -1: return
 135         self.dropdownlistbox.Select(toSel)
 136 
 137     def onListDClick(self, evt):
 138         self._setValueFromSelected()
 139 
 140     def onListColClick(self, evt):
 141         col = evt.GetColumn()
 142         #reverse the sort
 143         if col == self._colSearch:
 144             self._ascending = not self._ascending
 145         self.SortListItems( evt.GetColumn(), ascending=self._ascending )
 146         self._colSearch = evt.GetColumn()
 147         evt.Skip()
 148 
 149     def onEnteredText(self, event):
 150         text = event.GetString()
 151         if self._entryCallback:
 152             self._entryCallback()
 153         if not text:
 154             # control is empty; hide dropdown if shown:
 155             if self.dropdown.IsShown():
 156                 self._showDropDown(False)
 157             event.Skip()
 158             return
 159         found = False
 160         if self._multiChoices:
 161             #load the sorted data into the listbox
 162             dd = self.dropdownlistbox
 163             choices = [dd.GetItem(x, self._colSearch).GetText()
 164                 for x in xrange(dd.GetItemCount())]
 165         else:
 166             choices = self._choices
 167         for numCh, choice in enumerate(choices):
 168             if self._matchFunction and self._matchFunction(text, choice):
 169                 found = True
 170             elif choice.lower().startswith(text.lower()) :
 171                 found = True
 172             if found:
 173                 self._showDropDown(True)
 174                 item = self.dropdownlistbox.GetItem(numCh)
 175                 toSel = item.GetId()
 176                 self.dropdownlistbox.Select(toSel)
 177                 break
 178         if not found:
 179             self.dropdownlistbox.Select(self.dropdownlistbox.GetFirstSelected(), False)
 180             if self._hideOnNoMatch:
 181                 self._showDropDown(False)
 182         self._listItemVisible()
 183         event.Skip ()
 184 
 185     def onKeyDown ( self, event ) :
 186         """ Do some work when the user press on the keys:
 187             up and down: move the cursor
 188             left and right: move the search
 189         """
 190         skip = True
 191         sel = self.dropdownlistbox.GetFirstSelected()
 192         visible = self.dropdown.IsShown()
 193         KC = event.GetKeyCode()
 194         if KC == wx.WXK_DOWN :
 195             if sel < self.dropdownlistbox.GetItemCount () - 1:
 196                 self.dropdownlistbox.Select ( sel+1 )
 197                 self._listItemVisible()
 198             self._showDropDown ()
 199             skip = False
 200         elif KC == wx.WXK_UP :
 201             if sel > 0 :
 202                 self.dropdownlistbox.Select ( sel - 1 )
 203                 self._listItemVisible()
 204             self._showDropDown ()
 205             skip = False
 206         elif KC == wx.WXK_LEFT :
 207             if not self._multiChoices: return
 208             if self._colSearch > 0:
 209                 self._colSearch -=1
 210             self._showDropDown ()
 211         elif KC == wx.WXK_RIGHT:
 212             if not self._multiChoices: return
 213             if self._colSearch < self.dropdownlistbox.GetColumnCount() -1:
 214                 self._colSearch += 1
 215             self._showDropDown()
 216         if visible :
 217             if event.GetKeyCode() == wx.WXK_RETURN :
 218                 self._setValueFromSelected()
 219                 skip = False
 220             if event.GetKeyCode() == wx.WXK_ESCAPE :
 221                 self._showDropDown( False )
 222                 skip = False
 223         if skip :
 224             event.Skip()
 225 
 226     def onListItemSelected (self, event):
 227         self._setValueFromSelected()
 228         event.Skip()
 229 
 230     def onClickToggleDown(self, event):
 231         self._lastinsertionpoint = self.GetInsertionPoint()
 232         event.Skip ()
 233 
 234     def onClickToggleUp ( self, event ) :
 235         if self.GetInsertionPoint() == self._lastinsertionpoint :
 236             self._showDropDown ( not self.dropdown.IsShown() )
 237         event.Skip ()
 238 
 239     def onControlChanged(self, event):
 240         if self.IsShown():
 241             self._showDropDown( False )
 242         event.Skip()
 243 
 244     # -- Interfaces methods
 245     def SetMultipleChoices(self, choices, colSearch=0, colFetch=-1):
 246         ''' Set multi-column choice
 247         '''
 248         self._multiChoices = choices
 249         self._choices = None
 250         if not isinstance(self._multiChoices, list):
 251             self._multiChoices = list(self._multiChoices)
 252         flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING
 253         if not self._showHead:
 254             flags |= wx.LC_NO_HEADER
 255         self.dropdownlistbox.SetWindowStyleFlag(flags)
 256         #prevent errors on "old" systems
 257         if sys.version.startswith("2.3"):
 258             self._multiChoices.sort(lambda x, y: cmp(x[0].lower(), y[0].lower()))
 259         else:
 260             self._multiChoices.sort(key=lambda x: locale.strxfrm(x[0]).lower() )
 261         self._updateDataList(self._multiChoices)
 262         if len(choices)<2 or len(choices[0])<2:
 263             raise ValueError, "You have to pass me a multi-dimension list with at least two entries"
 264             # with only one entry, the dropdown artifacts
 265         for numCol, rowValues in enumerate(choices[0]):
 266             if self._colNames: colName = self._colNames[numCol]
 267             else: colName = "Select %i" % numCol
 268             self.dropdownlistbox.InsertColumn(numCol, colName)
 269         for numRow, valRow in enumerate(choices):
 270             for numCol, colVal in enumerate(valRow):
 271                 if numCol == 0:
 272                     index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
 273                 self.dropdownlistbox.SetStringItem(index, numCol, colVal)
 274                 self.dropdownlistbox.SetItemData(index, numRow)
 275         self._setListSize()
 276         self._colSearch = colSearch
 277         self._colFetch = colFetch
 278 
 279     def SetChoices(self, choices):
 280         '''
 281         Sets the choices available in the popup wx.ListBox.
 282         The items will be sorted case insensitively.
 283         '''
 284         self._choices = choices
 285         self._multiChoices = None
 286         flags = wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_SORT_ASCENDING | wx.LC_NO_HEADER
 287         self.dropdownlistbox.SetWindowStyleFlag(flags)
 288         if not isinstance(choices, list):
 289             self._choices = list(choices)
 290         #prevent errors on "old" systems
 291         if sys.version.startswith("2.3"):
 292             self._choices.sort(lambda x, y: cmp(x.lower(), y.lower()))
 293         else:
 294             self._choices.sort(key=lambda x: locale.strxfrm(x).lower())
 295         self._updateDataList(self._choices)
 296         self.dropdownlistbox.InsertColumn(0, "")
 297         for num, colVal in enumerate(self._choices):
 298             index = self.dropdownlistbox.InsertImageStringItem(sys.maxint, colVal, -1)
 299             self.dropdownlistbox.SetStringItem(index, 0, colVal)
 300             self.dropdownlistbox.SetItemData(index, num)
 301         self._setListSize()
 302         # there is only one choice for both search and fetch if setting a single column:
 303         self._colSearch = 0
 304         self._colFetch = -1
 305 
 306     def GetChoices(self):
 307         return self._choices or self._multiChoices
 308 
 309     def SetSelectCallback(self, cb=None):
 310         self._selectCallback = cb
 311 
 312     def SetEntryCallback(self, cb=None):
 313         self._entryCallback = cb
 314 
 315     def SetMatchFunction(self, mf=None):
 316         self._matchFunction = mf
 317 
 318     #-- Internal methods
 319     def _setValueFromSelected( self ) :
 320          '''
 321          Sets the wx.TextCtrl value from the selected wx.ListCtrl item.
 322          Will do nothing if no item is selected in the wx.ListCtrl.
 323          '''
 324          sel = self.dropdownlistbox.GetFirstSelected()
 325          if sel > -1:
 326             if self._colFetch != -1: col = self._colFetch
 327             else: col = self._colSearch
 328             itemtext = self.dropdownlistbox.GetItem(sel, col).GetText()
 329             if self._selectCallback:
 330                 dd = self.dropdownlistbox
 331                 values = [dd.GetItem(sel, x).GetText()
 332                     for x in xrange(dd.GetColumnCount())]
 333                 self._selectCallback( values )
 334             self.SetValue (itemtext)
 335             self.SetInsertionPointEnd ()
 336             self.SetSelection ( -1, -1 )
 337             self._showDropDown ( False )
 338 
 339     def _showDropDown ( self, show = True ) :
 340         '''
 341         Either display the drop down list (show = True) or hide it (show = False).
 342         '''
 343         if show :
 344             size = self.dropdown.GetSize()
 345             width, height = self . GetSizeTuple()
 346             x, y = self . ClientToScreenXY ( 0, height )
 347             if size.GetWidth() != width :
 348                 size.SetWidth(width)
 349                 self.dropdown.SetSize(size)
 350                 self.dropdownlistbox.SetSize(self.dropdown.GetClientSize())
 351             if y + size.GetHeight() < self._screenheight :
 352                 self.dropdown . SetPosition ( wx.Point(x, y) )
 353             else:
 354                 self.dropdown . SetPosition ( wx.Point(x, y - height - size.GetHeight()) )
 355         self.dropdown.Show ( show )
 356 
 357     def _listItemVisible( self ) :
 358         '''
 359         Moves the selected item to the top of the list ensuring it is always visible.
 360         '''
 361         toSel =  self.dropdownlistbox.GetFirstSelected ()
 362         if toSel == -1: return
 363         self.dropdownlistbox.EnsureVisible( toSel )
 364 
 365     def _updateDataList(self, choices):
 366         #delete, if need, all the previous data
 367         if self.dropdownlistbox.GetColumnCount() != 0:
 368             self.dropdownlistbox.DeleteAllColumns()
 369             self.dropdownlistbox.DeleteAllItems()
 370         #and update the dict
 371         if choices:
 372             for numVal, data in enumerate(choices):
 373                 self.itemDataMap[numVal] = data
 374         else:
 375             numVal = 0
 376         self.SetColumnCount(numVal)
 377 
 378     def _setListSize(self):
 379         if self._multiChoices:
 380             choices = self._multiChoices
 381         else:
 382             choices = self._choices
 383         longest = 0
 384         for choice in choices :
 385             longest = max(len(choice), longest)
 386         longest += 3
 387         itemcount = min( len( choices ) , 7 ) + 2
 388         charheight = self.dropdownlistbox.GetCharHeight()
 389         charwidth = self.dropdownlistbox.GetCharWidth()
 390         self.popupsize = wx.Size( charwidth*longest, charheight*itemcount )
 391         self.dropdownlistbox.SetSize ( self.popupsize )
 392         self.dropdown.SetClientSize( self.popupsize )
 393 
 394 class test:
 395     def __init__(self):
 396         args = {}
 397         if True:
 398             args["colNames"] = ("col1", "col2")
 399             args["multiChoices"] = [ ("Zoey","WOW"), ("Alpha", "wxPython"),
 400                                     ("Ceda","Is"), ("Beta", "fantastic"),
 401                                     ("zoebob", "!!")]
 402             args["colFetch"] = 1
 403         else:
 404             args["choices"] = ["123", "cs", "cds", "Bob","Marley","Alpha"]
 405         args["selectCallback"] = self.selectCallback
 406         self.dynamic_choices = [
 407                         'aardvark', 'abandon', 'acorn', 'acute', 'adore',
 408                         'aegis', 'ascertain', 'asteroid',
 409                         'beautiful', 'bold', 'classic',
 410                         'daring', 'dazzling', 'debonair', 'definitive',
 411                         'effective', 'elegant',
 412                         'http://python.org', 'http://www.google.com',
 413                         'fabulous', 'fantastic', 'friendly', 'forgiving', 'feature',
 414                         'sage', 'scarlet', 'scenic', 'seaside', 'showpiece', 'spiffy',
 415                         'www.wxPython.org', 'www.osafoundation.org'
 416                         ]
 417         app = wx.PySimpleApp()
 418         frm = wx.Frame(None,-1,"Test",style=wx.TAB_TRAVERSAL|wx.DEFAULT_FRAME_STYLE)
 419         panel = wx.Panel(frm)
 420         sizer = wx.BoxSizer(wx.VERTICAL)
 421         self._ctrl = TextCtrlAutoComplete(panel, **args)
 422         but = wx.Button(panel,label="Set other multi-choice")
 423         but.Bind(wx.EVT_BUTTON, self.onBtMultiChoice)
 424         but2 = wx.Button(panel,label="Set other one-colum choice")
 425         but2.Bind(wx.EVT_BUTTON, self.onBtChangeChoice)
 426         but3 = wx.Button(panel,label="Set the starting choices")
 427         but3.Bind(wx.EVT_BUTTON, self.onBtStartChoices)
 428         but4 = wx.Button(panel,label="Enable dynamic choices")
 429         but4.Bind(wx.EVT_BUTTON, self.onBtDynamicChoices)
 430         sizer.Add(but, 0, wx.ADJUST_MINSIZE, 0)
 431         sizer.Add(but2, 0, wx.ADJUST_MINSIZE, 0)
 432         sizer.Add(but3, 0, wx.ADJUST_MINSIZE, 0)
 433         sizer.Add(but4, 0, wx.ADJUST_MINSIZE, 0)
 434         sizer.Add(self._ctrl, 0, wx.EXPAND|wx.ADJUST_MINSIZE, 0)
 435         panel.SetAutoLayout(True)
 436         panel.SetSizer(sizer)
 437         sizer.Fit(panel)
 438         sizer.SetSizeHints(panel)
 439         panel.Layout()
 440         app.SetTopWindow(frm)
 441         frm.Show()
 442         but.SetFocus()
 443         app.MainLoop()
 444 
 445     def onBtChangeChoice(self, event):
 446         #change the choices
 447         self._ctrl.SetChoices(["123", "cs", "cds", "Bob","Marley","Alpha"])
 448         self._ctrl.SetEntryCallback(None)
 449         self._ctrl.SetMatchFunction(None)
 450 
 451     def onBtMultiChoice(self, event):
 452         #change the choices
 453         self._ctrl.SetMultipleChoices( [ ("Test","Hello"), ("Other word","World"),
 454                                         ("Yes!","it work?") ], colFetch = 1 )
 455         self._ctrl.SetEntryCallback(None)
 456         self._ctrl.SetMatchFunction(None)
 457 
 458     def onBtStartChoices(self, event):
 459         #change the choices
 460         self._ctrl.SetMultipleChoices( [ ("Zoey","WOW"), ("Alpha", "wxPython"),
 461                                     ("Ceda","Is"), ("Beta", "fantastic"),
 462                                     ("zoebob", "!!")], colFetch = 1 )
 463         self._ctrl.SetEntryCallback(None)
 464         self._ctrl.SetMatchFunction(None)
 465 
 466     def onBtDynamicChoices(self, event):
 467         '''
 468         Demonstrate dynamic adjustment of the auto-complete list, based on what's
 469         been typed so far:
 470         '''
 471         self._ctrl.SetChoices(self.dynamic_choices)
 472         self._ctrl.SetEntryCallback(self.setDynamicChoices)
 473         self._ctrl.SetMatchFunction(self.match)
 474 
 475     def match(self, text, choice):
 476         '''
 477         Demonstrate "smart" matching feature, by ignoring http:// and www. when doing
 478         matches.
 479         '''
 480         t = text.lower()
 481         c = choice.lower()
 482         if c.startswith(t): return True
 483         if c.startswith(r'http://'): c = c[7:]
 484         if c.startswith(t): return True
 485         if c.startswith('www.'): c = c[4:]
 486         return c.startswith(t)
 487 
 488     def setDynamicChoices(self):
 489         ctrl = self._ctrl
 490         text = ctrl.GetValue().lower()
 491         current_choices = ctrl.GetChoices()
 492         choices = [choice for choice in self.dynamic_choices if self.match(text, choice)]
 493         if choices != current_choices:
 494             ctrl.SetChoices(choices)
 495 
 496     def selectCallback(self, values):
 497         """ Simply function that receive the row values when the
 498             user select an item
 499         """
 500         print "Select Callback called...:",  values
 501 
 502 if __name__ == "__main__":
 503     test()

Comment by Franz Steinhaeusler

If I replace the init line of the textctrl,

when the Enter Key seems to be processed accordingly.

I see, your implementation to this is much better than mine. :)

Comment by Edward Flick

Thanks Franz I think we just about tied for the solution to that. I had just added the TE_PROCESS_ENTER dynamically in the init section.

Either way :-). So do you have any idea on how to fix the PopupWindow issue?

Comment by Franz Steinhaeusler

No, sorry. I will ask in the wxPython mailing list.

Michele Petrazzo posted a solution using a ListCtrl instead of a ListBox.

http://lists.wxwidgets.org/cgi-bin/ezmlm-cgi?11:mss:47543:200602:jjhfcaoagldbfbigklnf

Comment by Michele Petrazzo

New class uploaded

Comment by Michele Petrazzo

Add two interface methods: setMultipleChoices and setChoices that permit to modify the list after the initialization.

Comment by Edward Flick

Changed init to support single column list of choices (was broken). Also, changed the sort function to locale specific sort (as is used in the mixin). Commented out the mixin sort called at the beginning, since its unnecessary.

Comment by Michele Petrazzo

Change the sort methods for prevent the old (python 2.3) sort method that not accepting arguments

Comment by Will Sadkin

Fixed bug in design that could leave fetch column set to a non-existent column if you switched from a multi-choice to a single choice list, causing the control not to autocomplete.

Instead, .setChoices() now automatically sets colSearch and colFetch to 0 and -1, respectively, (as there's only one choice), and .setMultipleChoices() now takes colSearch and colFetch as arguments. (I also changed fetchCol to colFetch, to be consistent with colSearch and colNames.)

I also made the behavior more consistent with the Firefox address bar behavior, so that when the control is empty(ied), the dropdown is hidden, and added a constructor argument, hideOnNoMatch (by default True), so that when a user is typing, if nothing in the choice list matches, the dropdown is hidden as well.

(Finally, I fixed this wiki page to not put wiki links in where none were meant to be.)

Comment by Will Sadkin

Comment by Marc Hedlund

This class looks great -- thanks to the people who have worked on it. As a note so that others are aware, the class depends on PopupWindow, which is not currently (Jan 2008) implemented on Mac. If you need something cross-platform, you'll have to look elsewhere...

Comment by Will Sadkin

According to http://trac.wxwidgets.org/ticket/9377, as of 6/27/2008, wxWidgets was updated to support PopupWindow... I don't know how long it will take for wxPython to incorporate this change, but someone who has access to a Mac should check this out and update this page if this restriction no longer applies.)

Comment by Marcelo Fernandez

This widget is great! And it also works passing a cursor.fetchall() (from a SQLite DB) to the SetMultipleChoices() method. :-)

I was looking at the custom widget TextCtrl and implementing it in a little application I'm developing. The thing is, the widget works very well overall, but I added a new method which I needed:

   1     def GetSelectedItems(self):
   2         ''' Returns the complete listbox item selected as a list '''
   3         dd = self.dropdownlistbox
   4         sel = dd.GetFirstSelected()
   5         if sel > -1:
   6             values = [dd.GetItem(sel, x).GetText()
   7                       for x in xrange(dd.GetColumnCount())]
   8             return values
   9         else:
  10             return None

It borrows a lot of code from the internal "_setValueFromSelected()" method, but its intention is to return the selected listbox item as a list.

I needed it because in the "Save Form" button I wanted to know not only the TextCtrl value, but all the ListCtrl values selected; and I didn't want to set a callback to _selectCallback() (which adds more code).

IMHO, I think this approach to get all the ListCtrl values stored there, in any time, is cleaner.

Comment by Matthijs Sypkens Smit

I believe to have discovered a bug in the current code (29-7-2009). In lines 101-105 ('gp = self' to 'gp = gp.GetParent()') bindings are created to catch movement and resize events for parents. However, these are never removed when the control is terminated. The problem came to light when I put the control on a custom dialog. After the dialog had been destroyed I got PyDeadObjectErrors:

The following code fixed it, at least for my case. I inserted them between line 105 and 106, but there might be

   1     def handler_close(evt):
   2         # unbinding...
   3         gp = self
   4         while ( gp != None ) :
   5             gp.Unbind( wx.EVT_MOVE , gp, -1, -1)
   6             gp.Unbind( wx.EVT_SIZE , gp, -1, -1)
   7             gp = gp.GetParent()
   8 
   9     self.Bind(wx.EVT_WINDOW_DESTROY, handler_close)

Comment by Matthijs Sypkens Smit

This widget does not work perfectly when placed on a modal dialog. I suspect this is because the ListBox is on a separate popup window. Doubleclick on an item in the listbox or scrolling with the scrollbars does not work on a modal dialog. Scrolling with the keyboard does work.

Comment by James Hofmann

16 October 2009: Clarified the exact usage of multiChoices(the test was incorrect before).

Comment by Daniel Levine

10 July 2010: Checked with a friend who has wxPython on a Mac (wxPython 2.8.10.1) and got a non-implementation error. This still does not work for Macs.

TextCtrlAutoComplete (last edited 2011-04-01 07:41:43 by newacct)