Building an autofilter combobox in ttkbootstrap. why doesn't it not work with Toplevel windows?

57 Views Asked by At

I want to have an autofilter combobox in ttkbootstrap (FilteredCombobox). When you type any character in the entry box, the options list is shown just with the options that contain these characteres. I built it in base on a combination of an entry widget, a button, and a listbox (in a toplevel window). I tested it in a Main window (Window) and it runs fine but when I test it in a toplevel window it's not running. I have problem/conflit with the the TopLevel window that manages the application widgets and the TopLevel window that manages the listbox include in the FilteredCombobox. When you open the main toplevel window containing a FilteredCombobox, the Toplevel window included in the FilteredCombobox appears behind the main toplevel window. Then, when you push the button to show the list options the toplevel window con the listbox appears in the right site but without the focus. I cannot choose any option of the listbox.

import ttkbootstrap as tb
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from tkinter import Listbox

class FilteredCombobox(tb.Frame):
    def __init__(self, master, height=7, width=20, **kwargs):
        super().__init__(master, **kwargs)
        self.values = []
        self.listbox_height = height  
        self.entry_width = width

        self.showing_listbox_from_set = False

        self.entry_var = tb.StringVar()
        self.entry_frame = tb.Frame(self)
        self.entry = tb.Entry(self.entry_frame, textvariable=self.entry_var, width=self.entry_width)
        self.entry.pack(side=tb.LEFT, fill=tb.BOTH, expand=True)
        self.entry.bind('<KeyRelease>', self.on_keyrelease)
        self.bind("<FocusOut>", lambda event: self.validate_selection(master))
    
        self.drop_button = tb.Button(self.entry_frame, text='▼', width=3,command=self.toggle_listbox, takefocus=False, padding=(0,5,0,5), bootstyle="primary-outline")
        self.drop_button.pack(side=tb.RIGHT, padx=0, fill=tb.Y)
        self.entry_frame.pack(fill=tb.BOTH)
    
        self.listbox_top = tb.Toplevel(self)
        self.listbox_top.withdraw()
        self.listbox_top.overrideredirect(True)
        self.listbox = Listbox(self.listbox_top, height=self.listbox_height, width=self.entry_width)
        self.listbox.pack()
        self.listbox.bind('<<ListboxSelect>>', self.on_select)
        self.listbox.bind('<Motion>', self.on_motion)
    
        self.entry.bind('<FocusOut>', self.hide_listbox)
        self.listbox_top.bind('<FocusOut>', self.hide_listbox)
    
        self.entry.bind('<Tab>', self.handle_tab)
        self.entry.bind('<Return>', self.fill_with_first_option)
    
        self.entry_var.trace('w', self.update_list)


    def toggle_listbox(self):
        if self.listbox_top.winfo_viewable():
            self.hide_listbox()
        else:
            self.show_listbox()

    def show_listbox(self):
        self.update_list(full_list=True)
        self.position_listbox()
        self.listbox_top.deiconify()
        self.listbox.selection_clear(0, tb.END)

    def on_keyrelease(self, event):
        if event.keysym in ("Tab", "Return", "Up", "Down", "Left", "Right"):
            return
        self.update_list()

    def update_list(self, *args, full_list=False):
        typed_text = self.entry_var.get().lower()
        self.listbox.delete(0, tb.END)
        matches = self.values if full_list else [value for value in self.values if typed_text in value.lower()]
        for value in matches:
            self.listbox.insert(tb.END, value)
        if matches and not self.showing_listbox_from_set:
            self.position_listbox()
            self.listbox_top.deiconify()
            self.listbox.selection_set(0)
        else:
            self.listbox_top.withdraw()

    def position_listbox(self):
        x = self.entry_frame.winfo_rootx()
        y = self.entry_frame.winfo_rooty() + self.entry_frame.winfo_height()
        self.listbox_top.geometry(f"+{x}+{y}")

    def on_select(self, event):
        if self.listbox.curselection():
            selected = self.listbox.get(self.listbox.curselection())
            self.entry_var.set(selected)
        self.hide_listbox()

    def on_motion(self, event):
        index = self.listbox.nearest(event.y)
        self.listbox.selection_clear(0, tb.END)
        self.listbox.selection_set(index)

    def hide_listbox(self, event=None):
        self.listbox_top.withdraw()
    
    def handle_tab(self, event):
        if self.listbox_top.winfo_viewable():
            self.fill_with_first_option(event)
        self.showing_listbox_from_set = True
        self.after(100, lambda: setattr(self, "showing_listbox_from_set", False))


    def fill_with_first_option(self, event):
        if self.listbox.size() > 0 and self.listbox.curselection():
            self.on_select(None)
        self.entry.focus_set()
        return 'break'

    def get(self):
        return self.entry_var.get()

    def set(self, value):
        self.entry_var.set(value)
        self.showing_listbox_from_set = 
        self.update_list()
        self.showing_listbox_from_set = False  
    
    def set_completion_list(self, lista):   
        lista.sort()
        self.values = lista
        self.entry_var.set('')  
        self.listbox.delete(0, tb.END)
        for value in lista: 
            self.listbox.insert(tb.END, value)
        

    def current(self, index):
        if 0 <= index < len(self.values):
            self.entry_var.set(self.values[index])
            self.update_list()
            self.listbox.selection_clear(0, tb.END)
            self.listbox.selection_set(index)
            self.listbox.see(index)
        else:
            return
    
    def validate_selection(self, ventana):
        selected_value = self.get()
        if selected_value != '':
            if selected_value not in self.values:
                Messagebox.show_warning("Debes elegir una de las opciones disponibles", "Validación Combobox", parent=ventana)
                self.focus_set()
            
    def set_font(self, font):
        self.entry.config(font=font)
        self.listbox.config(font=font)
    
    def set_state(self, estado):
        if estado == "normal":
            self.entry.config(state=estado)
            self.listbox.config(state=estado)
        elif estado == "readonly":
            self.entry.config(state=estado)
            self.listbox.config(state="normal")
        elif estado == "disabled":
            self.entry.config(state="readonly")
            self.listbox.config(state="disabled")


    def delete(self, inicio, fin):
        self.entry.delete(inicio, fin)


class MainWindow:
    def __init__(self):
       self.root = tb.Window(themename="superhero")
       self.root.geometry("300x300+0+0")
       self.root.resizable(False, False)
       self.root.focus()
       self.root.grab_set()

       style = tb.Style()
       style.configure("primary.TButton", font = ("Calibri", 12, "bold"))
       style.configure("danger.TButton", font = ("Calibri", 12, "bold"))

       tb.Label(self.root, text="User", font = ("Calibri", 12, "bold")).pack(pady=10)
       lista_usuarios =['Felix', 'Raquel', 'Marta', 'Gonzalo']
       usuario = FilteredCombobox(self.root, height=5, width=20)
       usuario.set_completion_list(lista_usuarios)
       usuario.set_font(("Times Roman", 10))
       usuario.pack(pady=15)

       b_entrar= tb.Button(self.root, text="Enter", command=lambda: self.enter_app(), bootstyle="primary", style="primary.TButton")
       b_entrar.pack(pady=10, ipadx=8)
    
       b_salir= tb.Button(self.root, text="Exit", command=lambda: self.root.destroy(), bootstyle="danger", style="danger.TButton")
       b_salir.pack(pady=10, ipadx=8)

      self.root.mainloop()


   def enter_app(self):
        NextWindow()
                 
class  NextWindow(tb.Toplevel):
    def __init__(self):
        super().__init__()

       self.geometry("300x300+0+0")
       self.resizable(False, False)
       self.focus()
       self.grab_set()
    
       frame = tb.Frame(self)
       frame.pack(fill=tb.X)
       Label = tb.Label(frame, text="Combo")
       Label.grid(row=0, column=0, sticky=W)
       combo = FilteredCombobox(frame, width=30, height=5)
       combo.grid (row=0, column=1, sticky=W)
       valores = ["primero", "segundo", "tercero", "cuarto", "quinto", "sexto", "septimo"]
       combo.set_completion_list(valores)

def main():
    MainWindow()

if __name__ == "__main__":
    main()
0

There are 0 best solutions below