Auto Completion for TextCtrl not suggesting everything from result array

Hello everyone,

Target OS: Win10 and Linux (Ubuntu).
I am implementing the AutoComplete feature for a TextCtrl like this:

class MyAutoCompleter(wx.TextCompleterSimple):
    def __init__(self, max_entries=25):
        wx.TextCompleterSimple.__init__(self)
        self.max_entries = max_entries

    def GetCompletions(self, prefix):
        global auto_completion_entries

        res = []
        text = prefix.lower()

        # First add all entries starting with entered text.
        for entry in auto_completion_entries:
            if entry.strip().lower().startswith(text):
                res.append(entry)

            if len(res) >= self.max_entries:
                break

        # TODO: Below code is currently NOT working in wxPython!
        #       Keeping it here, in case it ever while be.
        # If there is still room, also add entries
        # which include the string anywhere within them.
        # if len(res) < self.max_entries:
        #     for entry in auto_completion_entries:
        #         if text in entry.strip().lower() and entry not in res:
        #             res.append(entry)
        # 
        #         if len(res) >= self.max_entries:
        #             break

        return res

# Later in Code for the TextCtrl:
TextCtrl.AutoComplete(MyAutoCompleter())

Auto completion works as long as an entry from the auto_completion_entries list is found which starts with the prefix.
As you can guess from the commented out code part auto completion doesn’t work if an entry is found in the auto_completion_entries list which includes the prefix anywhere within it.

I am just wondering whether this behaviour is intentional or maybe platform specific?

Kind regards,
Spyro

not long ago I have uploaded a context aware TextCtrl which is standard for a text editor or search in the WWW: give it a try & vent your opinion :partying_face:

Hi,

This seems to be the specification.
(wx.TextCompleterSimple — wxPython Phoenix 4.1.2a1 documentation)

GetCompletions (self , prefix )

Please notice that the returned values should start with the prefix, otherwise they will be simply ignored, making adding them to the array in the first place useless.

Ah… classic case of rtfm, my bad!
That’s what happens when you only read the code sample and simply adjust it to your needs instead of reading the documentation -.-
Thanks for making me aware of it!

I’ll switch my implementation to wx.TextCompleter.

The wx.TextCompleterSimple is a subclass of wx.TextCompleter and your prefix-issue won’t change.
If you want advanced auto-completion using TextCtrl, you may have to implement the auto-completion code and popup window like @da-dada is demonstrating. (or you may be able to find a library created by someone).
If multi-line style is OK, my recommendation is using stc.StyledTextCtrl. Refer to the method AutoCompShow (wx.stc.StyledTextCtrl — wxPython Phoenix 4.1.2a1 documentation) and see “demo/StyledTextCtrl_2.py” for how to use it.

Just read through the documentation (didn’t have the time during my last answer) and you’re right.

Thanks for the recommendations (@da-dada and the StyledTextCtrl). I’ll defintely look into that!
Single-line is the only use-case at the moment, but if StyledTextCtrl can be configured to only show one line that should work. Not sure though if the extended functionality of the StyledTextCtrl is a bit too much if just the auto completion feature is needed. Seems a bit like cracking a nut with a sledgehammer.

Kudos for your quick responses! :slight_smile:

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 :wink:

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()