I ended up doing my own little implementation of what I needed and called it SuggestionsTextCtrl. It’s definitely not perfect and serves what I need, but (as the HR people would say): it has potential to grow
For anyone interested here is a ready-to-use snippet, feel free to use and contribute:
import wx
class SuggestionsPopup(wx.PopupTransientWindow):
def __init__(self, parent, style, result_callback=None):
wx.PopupTransientWindow.__init__(self, parent, style)
self.result_callback = result_callback
self.panel = wx.Panel(self)
self.list_ctrl = wx.ListCtrl(self.panel, style=wx.LC_REPORT|wx.LC_NO_HEADER|wx.LC_SINGLE_SEL)
self.list_ctrl.InsertColumn(0, "")
self.list_ctrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self._evt_list_item_selected)
self.update_size()
def update_suggestions(self, suggestions=None):
suggestions = [] if suggestions == None else suggestions
self.list_ctrl.DeleteAllItems()
for i, entry in enumerate(suggestions):
self.list_ctrl.InsertItem(i, entry)
self.update_size()
def update_size(self):
item_count = self.list_ctrl.GetItemCount()
item_extra_border = 0
item_max_width = 0
item_max_height = 0
self.list_ctrl.SetColumnWidth(0, wx.LIST_AUTOSIZE)
if item_count > 0:
# Add some extra space to bottom and side of list_ctrl
# so items have some "breathing room" and don't stick
# to the panel's borders with every pixel.
item_extra_border = 5
item_max_width = self.list_ctrl.GetColumnWidth(0) + item_extra_border
# Move the column's border outside the panel
# so it doesn't show within the popup.
self.list_ctrl.SetColumnWidth(0, item_max_width)
# Note: All items in a ListCtrl seem to receive the same
# width and height when the ListCtrl is rendered.
# Fortunately they receive width and height of the
# biggest item in the ListCtrl, so we only need
# to check the first item for size calculations.
item_max_height = self.list_ctrl.GetItemRect(0, wx.LIST_RECT_LABEL).GetHeight()
# Calculate popup size from amount of items in the list_ctrl.
list_ctrl_height = item_count * item_max_height + item_extra_border
list_ctrl_size = (item_max_width, list_ctrl_height)
self.SetSize(list_ctrl_size)
self.panel.SetSize(list_ctrl_size)
self.list_ctrl.SetSize(list_ctrl_size)
def _evt_list_item_selected(self, event):
if self.result_callback != None:
self.result_callback(event.GetText())
self.Show(False)
class SuggestionsTextCtrl(wx.Panel):
def __init__(self, parent, max_entries=5, choices=None):
wx.Panel.__init__(self, parent)
self.choices = [] if choices == None else choices
self.max_entries = max_entries
text_ctrl_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.text_ctrl = wx.TextCtrl(self)
self.text_ctrl.Bind(wx.EVT_TEXT, self._evt_text)
text_ctrl_sizer.Add(self.text_ctrl, 1, wx.EXPAND)
self.suggestions_popup = SuggestionsPopup(self, wx.BORDER_NONE, self.update_text_ctrl)
self.SetSizer(text_ctrl_sizer)
self.Layout()
text_ctrl_sizer.Fit(self)
def update_text_ctrl(self, text=None):
if text == None:
return
# Set new value without emitting a wx.EVT_TEXT event.
self.text_ctrl.ChangeValue(text)
def _evt_text(self, event):
text = self.text_ctrl.GetValue().lower()
if len(text) > 0:
matching_choices = []
# First add all entries starting with entered text.
for entry in self.choices:
if entry.strip().lower().startswith(text):
matching_choices.append(entry)
if len(matching_choices) >= self.max_entries:
break
# If there is still room, also add entries
# which include the string anywhere within them.
if len(matching_choices) < self.max_entries:
for entry in self.choices:
if text in entry.strip().lower() and entry not in matching_choices:
matching_choices.append(entry)
if len(matching_choices) >= self.max_entries:
break
self.suggestions_popup.update_suggestions(matching_choices)
# Show suggestions_popup below text_ctrl.
# Update position here instead of __init__
# in case the Panel is moved in between wx.EVT_TEXTs.
pos = self.text_ctrl.ClientToScreen((0,0))
sz = self.text_ctrl.GetSize()
self.suggestions_popup.Position(pos, (0, sz[1]))
self.suggestions_popup.Show(True)
else:
self.suggestions_popup.Show(False)
class TrialPanel(wx.Panel):
def __init__(self, *args, **kw):
auto_completion_entries = [
"The",
"quick",
"brown",
"fox",
"jumps",
"over",
"the",
"lazy",
"dog"
]
wx.Panel.__init__(self, *args, **kw)
self.SuggestionsTextCtrl = SuggestionsTextCtrl(self, max_entries=5, choices=auto_completion_entries)
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.SuggestionsTextCtrl, 1)
self.SetSizer(sizer)
self.Layout()
sizer.Fit(self)
if __name__ == '__main__':
app = wx.App()
frame = wx.Frame (None, -1, 'SuggestionsTextCtrl Demo', size=(400, 200))
TrialPanel(frame)
frame.Show()
app.MainLoop()