6

I am trying to extend the ttk combobox class to allow autosuggestion. the code I have far works well, but I would like to get it to show the dropdown once some text has been entered without removing focus from the entry part of the widget.

The part I am struggling with is finding a way to force the dropdown, in the python docs I cannot find any mention of this, however in the tk docs I did find a post method I believe is supposed to do this, except it doesn't seem to be implemented in the python wrapper.

I also tried generating a down arrow key event once the autosuggest has taken place, however while this does show the dropdown it removes focus, and trying to set the focus after this event doesn't seem to work either (focus does not return)

Is anyone aware of a function I can use to achieve this?

The code I have is for python 3.3 using only standard libs:

class AutoCombobox(ttk.Combobox):
    def __init__(self, parent, **options):
        ttk.Combobox.__init__(self, parent, **options)
        self.bind("<KeyRelease>", self.AutoComplete_1)
        self.bind("<<ComboboxSelected>>", self.Cancel_Autocomplete)
        self.bind("<Return>", self.Cancel_Autocomplete)
        self.autoid = None

    def Cancel_Autocomplete(self, event=None):
        self.after_cancel(self.autoid) 

    def AutoComplete_1(self, event):
        if self.autoid != None:
            self.after_cancel(self.autoid)
        if event.keysym in ["BackSpace", "Delete", "Return"]:
            return
        self.autoid = self.after(200, self.AutoComplete_2)

    def AutoComplete_2(self):
        data = self.get()
        if data != "":
            for entry in self["values"]:
                match = True
                try:
                    for index in range(0, len(data)):
                        if data[index] != entry[index]:
                            match = False
                            break
                except IndexError:
                    match = False
                if match == True:
                    self.set(entry)
                    self.selection_range(len(data), "end")
                    self.event_generate("<Down>",when="tail")
                    self.focus_set()
                    break
            self.autoid = None
NorthCat
  • 9,643
  • 16
  • 47
  • 50
James Kent
  • 5,763
  • 26
  • 50
  • You can call *post*, but that does not solve your problem, bcause *post* create a new popdown list box, and delete it when it lose focus/esc-pressed: `self.tk.call('ttk::combobox::Post', self)` – falsetru Aug 31 '14 at 02:59
  • 1
    I guess then that means the only way to achieve what I'm after would be creating my own mega widget from a collection of an entry, a listbox and a button bundles in a frame, shame really as the rest of my code was so much simpler than all the examples I could find of an autocompleting combobox. although I couldn't find one that actually showed the dropdown while typing. thanks for the help though. – James Kent Aug 31 '14 at 19:22
  • @JamesKent I'm trying to do something similar, did you have any luck finding a solution? – The Beanstalk Apr 20 '16 at 16:58
  • no luck with reliably forcing the dropdown, however the rest of the autocomplete worked reasonably well. – James Kent Apr 22 '16 at 11:51

2 Answers2

1

A workaround that achieves this UX using tooltips is demonstrated below. This is implemented using PySimpleGUI, but should be easily adaptable to "pure" tkinter.

Resulting UI/UX

from functools import partial
from typing import Callable, Any

from fuzzywuzzy import process, fuzz
import PySimpleGUI as sg


# SG: Helper functions:
def clear_combo_tooltip(*_, ui_handle: sg.Element, **__) -> None:
    if tt := ui_handle.TooltipObject:
        tt.hidetip()
        ui_handle.TooltipObject = None


def show_combo_tooltip(ui_handle: sg.Element, tooltip: str) -> None:
    ui_handle.set_tooltip(tooltip)
    tt = ui_handle.TooltipObject
    tt.y += 40
    tt.showtip()


def symbol_text_updated(event_data: dict[str, Any], all_values: list[str], ui_handle: sg.Element) -> None:
    new_text = event_data[ui_handle.key]
    if new_text == '':
        ui_handle.update(values=all_values)
        return
    matches = process.extractBests(new_text, all_values, scorer=fuzz.ratio, score_cutoff=40)
    sym = [m[0] for m in matches]
    ui_handle.update(new_text, values=sym)

    # tk.call('ttk::combobox::Post', ui_handle.widget)  # This opens the list of options, but takes focus
    clear_combo_tooltip(ui_handle=ui_handle)
    show_combo_tooltip(ui_handle=ui_handle, tooltip="\n".join(sym))


# Prepare data:
all_symbols = ["AAPL", "AMZN", "MSFT", "TSLA", "GOOGL", "BRK.B", "UNH", "JNJ", "XOM", "JPM", "META", "PG", "NVDA", "KO"]

# SG: Layout
sg.theme('DarkAmber')
layout = [
    [
        sg.Text('Symbol:'),
        sg.Combo(all_symbols, enable_per_char_events=True, key='-SYMBOL-')
    ]
]

# SG: Window
window = sg.Window('Symbol data:', layout, finalize=True)
window['-SYMBOL-'].bind("<Key-Down>", "KeyDown")

# SG: Event loop
callbacks: dict[str: Callable] = {
    '-SYMBOL-': partial(symbol_text_updated, all_values=all_symbols, ui_handle=window['-SYMBOL-']),
    '-SYMBOL-KeyDown': partial(clear_combo_tooltip, ui_handle=window['-SYMBOL-']),
}
unhandled_event_callback = partial(lambda x: print(f"Unhandled event key: {event}. Values: {x}"))

while True:
    event, values = window.read()
    if event in (sg.WIN_CLOSED, 'Exit'):
        break
    callbacks.get(event, unhandled_event_callback)(values)


# SG: Cleanup
window.close()

This solution was inspired by this gist and this discussion.

Dev-iL
  • 23,742
  • 7
  • 57
  • 99
0

You do not need to inherit ttk.Combobox for this event; simply use event_generate to force the dropdown:

box = Combobox(...)
def callback(box):
    box.event_generate('<Down>')
Jun
  • 171
  • 4
  • 16