Luther
Luther

Reputation: 574

Clarify functionality of Tkinter Autocomplete Entry

I have some questions about this Tkinter autocomplete entry by Mitja Martini.

I've removed nonessential code and left the original author's comments. The autocomplete works fine if you type slow enough to let the highlighting take place after each character, but having to type slowly defeats the purpose of autocomplete, since little time is saved. The user will have to watch the flashing highlight or just type very slow if he doesn't want to watch for the flash.

Why can't I type fast without the selection getting unhighlighted and the cursor repositioning to the end of the filled-in word; the result is that fast typing appends characters to the end of the completed word.

How could this be fixed?

What is meant by "a new hit list" (conditional commented out). I'd like to see a simple example of how this would work and/or why I can't just delete this condition.

What's meant by "known hit list" (section commented out). What's an example of an unknown hit list in this code, so I can see the commented lines at work?

import tkinter as tk

class AutocompleteEntry(tk.Entry):
    def set_completion_list(self, completion_list):
        self._completion_list = completion_list
        self._hits = []
        self._hit_index = 0
        self.position = 0
        self.bind('<KeyRelease>', self.handle_keyrelease)

    def autocomplete(self):

        # set position to end so selection starts where textentry ended
        self.position = len(self.get())

        # collect hits
        _hits = []
        for element in self._completion_list:
            if element.lower().startswith(self.get().lower()):
                _hits.append(element)

        # if we have a new hit list, keep this in mind 
        # if _hits != self._hits:
        self._hit_index = 0
        self._hits =_hits

        # # only allow cycling if we are in a known hit list
        # if _hits == self._hits and self._hits:
            # self._hit_index = (self._hit_index) % len(self._hits)

        # perform the auto completion
        if self._hits:
            self.delete(0,tk.END)
            self.insert(0,self._hits[self._hit_index])
            self.select_range(self.position,tk.END)

    def handle_keyrelease(self, event):
        if len(event.keysym) == 1:
            self.autocomplete()

def test(test_list):
    root = tk.Tk(className=' AutocompleteEntry demo')
    root.geometry('+500+300')
    entry = AutocompleteEntry(
        root, 
        fg='white', bg='black',
        insertbackground='white', 
        font=('arial', 30))
    entry.set_completion_list(test_list)
    entry.grid()
    entry.focus_set()
    root.mainloop()

if __name__ == '__main__':
    test_list = (
        'Geronimo', 'Tecumseh', 'onomatopoeia', 
        'onerous', 'technicality', 'geriatric' )
    test(test_list)

Upvotes: 1

Views: 6154

Answers (2)

Fred Lake
Fred Lake

Reputation: 11

import tkinter as tk

class AutocompleteEntry(tk.Entry):
    def initialize(self, completion_list):
        self.completion_list = completion_list
        self.hitsIndex = 0
        self.hits = []
        self.autocompleted = False
        self.current = ""
        self.bind('<KeyRelease>', self.act_on_release)
        self.bind('<Key>', self.act_on_press)


        
    def autocomplete(self):
        self.current = self.get().lower()
        self.hits = []
        for item in self.completion_list:
            if item.lower().startswith(self.current):
                self.hits.append(item)

        self.hitsIndex = 0
        if self.hits:
            self.display_autocompletion()

    def remove_autocompletion(self):
        cursor = self.index(tk.INSERT)
        self.delete(cursor, tk.END)
        self.autocompleted = False

    def display_autocompletion(self):

        cursor = self.index(tk.INSERT)
        self.delete(0, tk.END)
        self.insert(0, self.hits[self.hitsIndex])
        self.select_range(cursor, tk.END)


        self.icursor(cursor)
        self.autocompleted = True

    def act_on_release(self, event):
        return


    
    def act_on_press(self, event):
        if event.keysym in ('Left'):
            if self.autocompleted:
                self.remove_autocompletion()
                return "break"

        if event.keysym in ('Down', 'Up', 'Tab'):
            if self.select_present():
                cursor = self.index(tk.SEL_FIRST)
                # print("Cursor",cursor)
                # print("In box:",self.get().lower()[0:cursor])
                # print("Last entered",self.current)
                if len(self.hits) and self.current == self.get().lower()[0:cursor]:
                    if event.keysym == 'Up':
                        self.hitsIndex -= 1
                    else:
                        self.hitsIndex += 1
                    self.hitsIndex %= len(self.hits)
                    self.display_autocompletion()
            else:
                self.autocomplete()
            return "break"    
        
        if event.keysym in ('Right'):
            if self.select_present():
                self.selection_clear()
                self.icursor(tk.END)
                return "break"




# Example usage:
root = tk.Tk()

chiefs = [
    'Sitting Bull', 'Sitting Horse', 'Geronimo', 'Tecumseh', 'Pontiac', 
    'Red Cloud', 'Crazy Horse', 'Cochise', 'Red Jacket', 
    'Red Czar', 'Red Czechoslovakian']

auto_entry = AutocompleteEntry(root)
auto_entry.pack()
auto_entry.initialize(chiefs)
root.mainloop()

Upvotes: 1

Luther
Luther

Reputation: 574

The problem with the autocomplete code is that it relies on replacement of highlighted text so user has to wait for text to highlight before typing the next character. After posting my question, I wrote my own autocomplete from scratch which uses a different technique that allows the user to keep typing after the word has filled in. That doesn't answer the questions as stated above but it solves my immediate problem unless this code has a glitch I haven't found yet. The purpose of this partial answer is to show an autofill that doesn't prevent the user from typing after the word has filled in.

import tkinter as tk

chiefs = [
    'Sitting Bull', 'Geronimo', 'Tecumseh', 'Pontiac', 
    'Red Cloud', 'Crazy Horse', 'Cochise', 'Red Jacket', 
    'Red Czar', 'Red Czechoslovakian']

def match_string():
    hits = []
    got = auto.get()
    for item in chiefs:
        if item.startswith(got):
            hits.append(item)
    return hits    

def get_typed(event):
    if len(event.keysym) == 1:
        hits = match_string()
        show_hit(hits)

def show_hit(lst):
    if len(lst) == 1:
        auto.set(lst[0])
        detect_pressed.filled = True

def detect_pressed(event):    
    key = event.keysym
    if len(key) == 1 and detect_pressed.filled is True:
        pos = autofill.index(tk.INSERT)
        autofill.delete(pos, tk.END)

detect_pressed.filled = False

root = tk.Tk()

auto = tk.StringVar()

autofill = tk.Entry(
    root, 
    font=('tacoma', 30),
    bg='black',
    insertbackground='white',
    fg='white',
    textvariable=auto)
autofill.grid()
autofill.focus_set()
autofill.bind('<KeyRelease>', get_typed)
autofill.bind('<Key>', detect_pressed)

root.mainloop()

Upvotes: 7

Related Questions