3

I've been working on a command line tool for which I'm now making a PyQT GUI. I'd like to take my current autocomplete implementation using the readline module, and put that in a QLineEdit text box. Is this possible? Do you have any recommendations?

Here's an example of the stuff I'm doing with the readline module:

import readline

values = ['these','are','my','autocomplete','words']
completions = {}

def completer(text,state):
    try:
        matches = completions[text]
    except KeyError:
        matches = [value for value in values if text.upper() in value.upper()]
        completions[text] = matches
    try:
        return matches[state]
    except IndexError:
        return None

readline.set_completer(completer)
readline.parse_and_bind('tab: menu-complete')

whie 1:
    text = raw_input('> ')
    text.dostuff()

Ultimately, if I can't get the readline module to work in a QLineEdit widget, what I'd ultimately like to do is complete on a list of words, with the ability to have multiple words separated by symbols like +-*/() etc...

Thanks!

Bradley Powers
  • 717
  • 7
  • 19

1 Answers1

4

I can tell you that first off, it is a huge pain to try and wrap a QCompleter around new functionality. You have to be able to satisfy all of the QCompleter's interface, and bridge it around that realine code.

You have to manually update a QStringListModel set on the QCompleter, and provide the implementation of getting the current completion, and the total number of completions, for the given search prefix.

Here is a working example that is compatible with the PopupCompletion mode:

import re

class ReadlineCompleter(QtGui.QCompleter):

    def __init__(self, completeFn, *args, **kwargs):
        super(ReadlineCompleter, self).__init__(*args, **kwargs)
        self._completer = completeFn
        self.setModel(QtGui.QStringListModel())
        self.update()

    def setCompletionPrefix(self, val):
        super(ReadlineCompleter, self).setCompletionPrefix(val)
        self.update()

    def currentCompletion(self):
        state = self.currentRow()
        return self._completionAt(state)

    def completionCount(self):
        state = 0
        while True:
            result = self._completionAt(state)
            if not result:
                break
            state += 1
        return state

    def update(self):
        matches = [self._completionAt(i) for i in xrange(self.completionCount())]
        self.model().setStringList(matches)

    def _completionAt(self, state):
        text = str(self.completionPrefix())

        # regex to split on any whitespace, or the char set +*/^()-
        match = re.match(r'^(.*)([\s+*/^()-]+)(.*)$', text)
        if match:
            prefix, sep, text = match.groups()

        result = self._completer(str(text), state)

        if result and match:
            result = sep.join([prefix, result])

        return '' if result is None else result     

Notice in the _completionAt() method, I added the extra functionality you wanted, for detecting a separator pattern. You can adjust this obviously. But it will split off the last part and use that value to check the completion, then rejoin the result with the prefix again.

Usage

Important. You need to connect the textChanged signal from the QLineEdit to the completer to force an update. Otherwise none of the functionality will be used in the completer.

line = QtGui.QLineEdit()
comp = ReadlineCompleter(completer)
comp.setCompletionMode(comp.PopupCompletion)
line.setCompleter(comp)
# important
line.textChanged.connect(comp.setCompletionPrefix)

There are examples here showing how other people have had to fill in functionality in a custom line edit, where they completely go around the standard signaling of the completer and trigger it themselves. You can see its a little bit of effort.

jdi
  • 90,542
  • 19
  • 167
  • 203
  • The only functionality that I'd also like to get out of this which is offered in the readline module, is continuing to do tab completion across multiple words. For instance, if my list of words is aardvark aardwolf I want to be able to type in: "aardvark-aardw" and hit tab, then hav e it complete. Any ideas? Apologies if this is a simple issue, I'm very new to PyQt... – Bradley Powers Jul 09 '12 at 21:54
  • Ok, I just figured out the closest I can get to making this work without having to subclass a QLineEdit. I also added your part about detecting a separator and continuing completion. – jdi Jul 09 '12 at 22:36
  • Excellent, thanks very much for your help! There are two things that your current implementation don't do that I'd like to see. One is that it only separates on hyphens, and in my OP, I mentioned wanting to split on multiple operators like +-*/^(). My knowledge of regex is effectively zero, but I'm guessing that's achievable? Secondly, this will only split once. For instance, I still want tab completion if I do 'aardvark+aardwolves*bab' to match babel in my list of words. Thoughts? – Bradley Powers Jul 10 '12 at 14:27
  • Yes its all about the regex. You just need to define how to split it. It doesnt matter how many separators. As long as the split returns the two parts it will work. Learning regex is a separate task ;) hopefully you dont make that part of the requirement to accept this answer. – jdi Jul 10 '12 at 14:57
  • Fair enough. I just wanted to make sure that was all there was to it. Thanks for your help! – Bradley Powers Jul 10 '12 at 16:14
  • I just updated the regex to include the pattern you specified in the above comments. `\s` means "whitespace characters', and followed by the other explicit characters you mentioned. You just add them to that bracket section. – jdi Jul 10 '12 at 17:15
  • Thanks! I actually looked into regexes and figured that one out! They're an excellent tool. I haven't had much use for them before, as I'm a roboticist, and do very little general purpose programming like I am now, but it's a good tool for the toolkit. – Bradley Powers Jul 10 '12 at 21:31