-1

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)]
Joop
  • 3,706
  • 34
  • 55
  • 1
    Can you add enough code to make your code runnable so that I can test it? – Novel Apr 09 '17 at 23:48
  • 1
    Your question would be easier to understand if you removed all code that was unrelated to the problem, and added enough code to actually reproduce the problem. See and follow the advice here: [How to create a Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) – Bryan Oakley Apr 10 '17 at 00:45
  • Sorry I did not provide the most minimalistic working code. I should have. However the provided answer did provide the solution! You guys are awesome, thank you. I was stuck at this for a long time. – Joop Apr 10 '17 at 07:59

3 Answers3

1

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.

Taku
  • 31,927
  • 11
  • 74
  • 85
1

Unfortunately, it's really hard to understand your real problem because you've posted too much unrelated code and not enough related code. It seems to me that what you're trying to accomplish is to let the user press the down or up arrow while an entry has focus, and have that cause the selection in a listbox to move down or up. Further, it seems that part of the problem is that you're seeing characters in the entry widget that you do not want to see when you press down or up.

If that is the problem, the solution is fairly simple. All you need to do is have your binding return the string "break" to prevent the default binding from being processed. It is the default binding that is inserting the character.

Here is an example. Run the example, and press up and down to move the selection of the listbox. I've left out all of the code related to autocomplete so you can focus on how the event binding works.

import Tkinter as tk

class Example(object):
    def __init__(self):

        self.root = tk.Tk()

        self.entry = tk.Entry(self.root)
        self.listbox = tk.Listbox(self.root, exportselection=False)
        for i in range(30):
            self.listbox.insert("end", "Item #%s" % i)

        self.entry.pack(side="top", fill="x")
        self.listbox.pack(side="top", fill="both", expand=True)


        self.entry.bind("<Down>", self.handle_updown)
        self.entry.bind("<Up>", self.handle_updown)

    def start(self):
        self.root.mainloop()

    def handle_updown(self, event):
        delta = -1 if event.keysym == "Up" else 1
        curselection = self.listbox.curselection()
        if len(curselection) == 0:
            index = 0
        else:
            index = max(int(curselection[0]) + delta, 0)

        self.listbox.selection_clear(0, "end")
        self.listbox.selection_set(index, index)

        return "break"

if __name__ == "__main__":
    Example().start()

For a fairly thorough explanation of what happens when an event is triggered, see this answer: https://stackoverflow.com/a/11542200/7432

Community
  • 1
  • 1
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
0

Again, leaving aside the autocomplete requirement, I came up with a solution that uses that standard available commands and events for Listbox and Scrollbar. <<ListboxSelect>> lets you capture changes in selection from any of the lists and align the others. In addition, the Scrollbar and Listbox callbacks are directed to a routing function that passes things on to all of the listboxes.

# updownmultilistbox.py
# 7/24/2020
#
# incorporates vsb to propagate scrolling across lists
#

import tkinter as tk


class Example(object):
    def __init__(self):
        self.root = tk.Tk()
        self.listOfListboxes = []
        # self.active_lb = None
        self.vsb = tk.Scrollbar(orient='vertical', command=self.OnVsb)
        self.vsb.pack(side='right', fill='y')

        self.lb1 = tk.Listbox(self.root, exportselection=0,
                              selectmode= tk.SINGLE, yscrollcommand=self.vsb_set)
        self.lb2 = tk.Listbox(self.root, exportselection=0,
                              selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)
        self.lb3 = tk.Listbox(self.root, exportselection=0,
                              selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)

        self.listOfListboxes.append(self.lb1)
        self.listOfListboxes.append(self.lb2)
        self.listOfListboxes.append(self.lb3)

        for i in range(30):
            self.lb1.insert("end", "lb1 Item #%s" % i)
            self.lb2.insert("end", "lb2 Item #%s" % i)
            self.lb3.insert("end", "lb3 Item #%s" % i)

        self.lb1.pack(side="left", fill="both", expand=True)
        self.lb2.pack(side="left", fill="both", expand=True)
        self.lb3.pack(side="left", fill="both", expand=True)

        for lb in self.listOfListboxes:
            lb.bind('<<ListboxSelect>>', self.handle_select)

        for lb in self.listOfListboxes:
            lb.selection_set(0)
            lb.activate(0)
        self.listOfListboxes[0].focus_force()

    def start(self):
        self.root.title('updownmultilistbox')
        self.root.mainloop()

    def OnVsb(self, *args):
        for lb in self.listOfListboxes:
            lb.yview(*args)

    def vsb_set(self, *args):
        print ('vsb_set args: ', *args)
        self.vsb.set(*args)
        for lb in self.listOfListboxes:
            lb.yview_moveto(args[0])


    def handle_select(self, event):
        # set evey list to the same selection
        print ('select handler: ', event, event.widget.curselection())
        # self.active_lb = event.widget
        for lb in self.listOfListboxes:
            if lb != event.widget:
                lb.selection_clear(0, 'end')    # have to avoid this for the current widget
                lb.selection_set(event.widget.curselection())
                lb.activate(event.widget.curselection())
if __name__ == "__main__":
    Example().start()

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
mlr94549
  • 11
  • 1