pythontkinterautocompletelistboxpopup

Python autocomplete popup: double mouse selection


I am creating an autocomplete popup in Python using a Listbox which pops up. One can navigate by using arrows and using Enter to select or use the mouse to select an option. The arrow navigation is working perfectly; however, when I use the mouse to select an option and I want to use the arrows again, it automatically selects the second item on the list. Here is the code:

import tkinter as tk

masterlist = ["Daniel", "Ronel", "Dana", "Elani", "Isabel", "Hercules", "Karien", "Amor", "Piet", "Koos", "Jan", "Johan", "Denise", "Jean", "Petri"]
global listbox_wiggie_index

def new_list_for_listbox(*args):
    # When text in entry.wiggie is changed,
    # "search_var = tk.StringVar()" will change, which will activate this function.

    global listbox_wiggie_index
    
    listbox_wiggie_index = -1  # Reset index every time entry_wiggie changes so listbox is updated
    
    # Clean listbox_wiggie
    listbox_wiggie.delete(0, tk.END)

    # Get value of changeable search_var.
    t = search_var.get()
    
    if t == "":
        # Hide listbox_wiggie
        # If "listbox_wiggie.destroy()" is used, the widget will have to be created again
        listbox_wiggie.place_forget()
        return
    
    else:
        # Populate listbox_wiggie
        t = search_var.get()
        for e in masterlist:
            if t.lower() in e.lower():
                listbox_wiggie.insert(tk.END, e)
        
        place_listbox_wiggie()

    print(f"|new_list_for_listbox|New list was generated and listbox_wiggie was placed. Index = {listbox_wiggie_index}")

def place_listbox_wiggie():
    # The listbox_wiggie should be already populated. It will be placed at the bottom of entry_wiggie

    # Get coordinates of bottom left of entry_wiggie.
    x = entry_wiggie.winfo_rootx() - root.winfo_rootx()
    y = entry_wiggie.winfo_rooty() - root.winfo_rooty() + entry_wiggie.winfo_height()
    
    # Place listbox_wiggie at the bottom of entry_wiggie
    listbox_wiggie.place(x=x, y=y)
    entry_wiggie.focus_set()

def listbox_mouse_selection(event=None):
    # From:   "listbox_wiggie.bind("<<ListboxSelect>>", listbox_mouse_selection)"
    i = listbox_wiggie.curselection()[0]    # i = integer
    t = listbox_wiggie.get(i)               # text of selection
    print("|listbox_mouse_selection| Selection was made using mouse")
    write_value_to_label(t)


def write_value_to_label(teks):
    # Write selection to label_wiggie
    global listbox_wiggie_index
    
    label_wiggie.config(text=teks)
    # Hide listbox_wiggie
    listbox_wiggie.place_forget()
    entry_wiggie.focus_set()
    entry_wiggie.delete(0, tk.END)
    listbox_wiggie_index = -1
    print(f"|write_value_to_label|{teks} was written to label_wiggie")

def change_listbox_selection_after_arrow_press():
    # Focus on listbox_wiggie after arrows are pressed
    global listbox_wiggie_index

    # Remove the selection highlight from every item in the Listbox, starting from index 0 to the last item (tk.END)
    listbox_wiggie.selection_clear(0, tk.END)
    # Select the new item according to the index "listbox_wiggie_index":
    listbox_wiggie.selection_set(listbox_wiggie_index)
    # Highlight the item as the active item:
    listbox_wiggie.activate(listbox_wiggie_index)
    print(f'|change_listbox_selection_after_arrow_press|Selector of listbox_wiggie was changed to = {listbox_wiggie_index}')

def on_entry_wiggie_key_press(event):
    # Function is activated by:    entry_wiggie.bind("<KeyRelease>", on_entry_wiggie_key_press)

    global listbox_wiggie_index

    if listbox_wiggie.size() > 0:
        if event.keysym == "Down":
            print(f'|on_entry_wiggie_key_press|Down was pressed. Before pressing, index = {listbox_wiggie_index}')
            listbox_wiggie_index += 1
            if listbox_wiggie_index >= listbox_wiggie.size():
                listbox_wiggie_index = 0
            change_listbox_selection_after_arrow_press()

        elif event.keysym == "Up":
            print('|on_entry_wiggie_key_press|Up was pressed. Before pressing, index = {listbox_wiggie_index}')
            listbox_wiggie_index -= 1
            if listbox_wiggie_index < 0:
                listbox_wiggie_index = listbox_wiggie.size() - 1
            change_listbox_selection_after_arrow_press()

        elif event.keysym == "Return":
            print('|on_entry_wiggie_key_press|Return was pressed.')
            # check to ensure listbox_wiggie_index is valid before calling listbox_wiggie.get()
            if 0 <= listbox_wiggie_index < listbox_wiggie.size():
                write_value_to_label(listbox_wiggie.get(listbox_wiggie_index))

        elif event.keysym == "Escape":
            print('|on_entry_wiggie_key_press|Escape was pressed.')
            # Clear the listbox
            listbox_wiggie.delete(0, tk.END)
            listbox_wiggie.place_forget()
            entry_wiggie.focus_set()

        else:
            print(f'|on_entry_wiggie_key_press|{event.keysym} was pressed.')
            listbox_wiggie_index = -1
    
    else:
        print(f'|on_entry_wiggie_key_press|{event.keysym} was pressed.')
        listbox_wiggie_index = -1

    print(f'|on_entry_wiggie_key_press|search_var = "{search_var.get()}". Focus = {root.focus_get()}. Indeks = {listbox_wiggie_index}.  ...curselection()[0] = {listbox_wiggie.curselection()}')
    print()

root = tk.Tk()
root.title("Listbox appear after typing")
root.geometry("400x200")

# search_var is a variable which holds the content of entry_wiggie.
search_var = tk.StringVar()
search_var.trace("w", new_list_for_listbox)
# new_list_for_listbox will only be activated when search_var is changed (when contents of entry_wiggie is changed)

# Create entry_wiggie. Link content to StringVar "search_var"
entry_wiggie = tk.Entry(root, textvariable=search_var, width=60)
entry_wiggie.pack(pady=5)
entry_wiggie.focus_set()
entry_wiggie.bind("<KeyRelease>", on_entry_wiggie_key_press)

# Create listbox_wiggie, but do not place it (yet)
listbox_wiggie = tk.Listbox(root)
listbox_wiggie.bind("<<ListboxSelect>>", listbox_mouse_selection)
listbox_wiggie_index = -1

# Create label_wiggie to display result
label_wiggie = tk.Label(root, text="No option is selected.")
label_wiggie.pack(pady=5)

root.mainloop()

Solution

  • The main problem is in this line:

    listbox_wiggie.bind("<<ListboxSelect>>", listbox_mouse_selection)
    

    if you change this to:

    listbox_wiggie.bind("<ButtonRelease-1>", listbox_mouse_selection)
    

    Everything works!

    Why?

    There are two main concepts, active item and selection.

    <<ListboxSelect>> gets triggered when the selection changes. Meaning it gets triggered when the mouse clicks on an item.

    Even though that might look fine, the core problem is that if a selection exists, the arrows change the selection, NOT the active item like it usually does. Meaning the listbox_mouse_selection function gets triggered, which causes it to 'autocomplete' an item in the list.

    Unlike <<ListboxSelect>> , the <ButtonRelease-1> only triggers when the left mouse button is released, meaning it does not trigger when the arrow keys change the selection which fixes the bug.

    Feel free to ask any questions!