Search code examples
pythontkinterkey-bindings

Tkinter how to catch this particular key event


Problem Description

I have an application in Tkinter that uses a Listbox that displays search results. When I press command + down arrow key, I am putting the focus from the search field to the first item in the Listbox. This is exactly how I want the behaviour but instead for just the down arrow.

However, I am already binding the down arrow to this Listbox by self.bind("<Down>", self.moveDown). I can not understand why command + down works while simply down (to which I literally bind'ed it to) does not. Specifically the result of pressing the down arrow is the following enter image description here While pressing command + down gives the intended result:enter image description here How can I let down behave just like command + down, and what is the reason why command is required at all?

Code snippets

def matches(fieldValue, acListEntry):
    pattern = re.compile(re.escape(fieldValue) + '.*', re.IGNORECASE)
    return re.match(pattern, acListEntry)
root = Tk()
img = ImageTk.PhotoImage(Image.open('imgs/giphy.gif'))
panel = Label(root, image=img)
panel.grid(row=1, column=0)
entry = AutocompleteEntry(autocompleteList, panel, root, matchesFunction=matches)
entry.grid(row=0, column=0)
root.mainloop()

With AutocompleteEntry being:

class AutocompleteEntry(Tkinter.Entry):
    def __init__(self, autocompleteList, df, panel, rdi, *args, **kwargs):
        self.df = df
        self.product_row_lookup = {key:value for value, key in enumerate(autocompleteList)}
        temp = df.columns.insert(0, 'Product_omschrijving')
        temp = temp.insert(1, 'grams')
        self.result_list = pd.DataFrame(columns=temp)
        self.panel = panel
        self.rdi = rdi
        # self.bind('<Down>', self.handle_keyrelease)

        # Listbox length
        if 'listboxLength' in kwargs:
            self.listboxLength = kwargs['listboxLength']
            del kwargs['listboxLength']
        else:
            self.listboxLength = 8
        # Custom matches function
        if 'matchesFunction' in kwargs:
            self.matchesFunction = kwargs['matchesFunction']
            del kwargs['matchesFunction']
        else:
            def matches(fieldValue, acListEntry):
                pattern = re.compile('.*' + re.escape(fieldValue) + '.*', re.IGNORECASE)
                return re.match(pattern, acListEntry)
            self.matchesFunction = matches

        Entry.__init__(self, *args, **kwargs)
        self.focus()
        self.autocompleteList = autocompleteList
        self.var = self["textvariable"]
        if self.var == '':
            self.var = self["textvariable"] = StringVar()

        self.var.trace('w', self.changed)
        self.bind("<Right>", self.selection)
        self.bind("<Up>", self.moveUp)
        self.bind("<Down>", self.moveDown)
        self.bind("<Return>", self.selection)
        self.listboxUp = False
        self._digits = re.compile('\d')


    def changed(self, name, index, mode):
        if self.var.get() == '':
            if self.listboxUp:
                self.listbox.destroy()
                self.listboxUp = False
        else:
            words = self.comparison()
            if words:
                if not self.listboxUp:
                    self.listbox = Listbox(width=self["width"], height=self.listboxLength)
                    self.listbox.bind("<Button-1>", self.selection)
                    self.listbox.bind("<Right>", self.selection)
                    self.listbox.bind("<Down>", self.moveDown)
                    self.listbox.bind("<Tab>", self.selection)
                    self.listbox.place(x=self.winfo_x(), y=self.winfo_y() + self.winfo_height())
                    self.listboxUp = True

                self.listbox.delete(0, END)
                for w in words:
                    self.listbox.insert(END, w)
            else:
                if self.listboxUp:
                    self.listbox.destroy()
                    self.listboxUp = False
                else:
                    string = self.get()
                    if '.' in string:
                        write_to_file(self, string)

    def contains_digits(self, d):
        return bool(self._digits.search(d))


    def selection(self, event):
        if self.listboxUp:
            string = self.listbox.get(ACTIVE)
            self.var.set(string + ' ')
            self.listbox.destroy()
            self.listboxUp = False
            self.icursor(END)



    def moveDown(self, event):
        self.focus()
        if self.listboxUp:
            if self.listbox.curselection() == ():
                index = '0'
                print "ok"
            else:
                index = self.listbox.curselection()[0]
                print "blah"

            if index != END:
                self.listbox.selection_clear(first=index)
                print "noo"
                if index != '0':
                    index = str(int(index) + 1)

            self.listbox.see(index)  # Scroll!
            self.listbox.selection_set(first=index)
            self.listbox.activate(index)
        else:
            print "not up"


    def comparison(self):
        return [w for w in self.autocompleteList if self.matchesFunction(self.var.get(), w)]

Solution

  • Both command+down and down should produce the same output excepted that down also types question mark onto the entry which made the last letter typed is the question mark box.

    This is because pressing command, your computer checks the option menu to see if there's a shortcut with that key, if there isn't any, it will not do anything. While tkinter registered the down button as being pressed, so the event was triggered.

    In contrast, With out pressing command, the Entry first displays the value of "down", which there isn't any, then executes the event binding, what you can do is, in the event, remove the last letter of the Entry. You can do so by self.delete(len(self.get())-1) in your event. Or add a return 'break' at the end of your event to prevent it from being typed.