Welcome, guest | Sign In | My Account | Store | Cart
# Author: Miguel Martinez Lopez

import re
import os

try:
    from Tkinter import StringVar, Entry, Frame, Listbox, Button
    from ttk import Scrollbar
    from Tkconstants import *
except ImportError:
    from tkinter import StringVar, Entry, Frame, Listbox, Button
    from tkinter.ttk import Scrollbar
    from tkinter.constants import *

try:
    from tkFileDialog import *
except ImportError:
    from tkinter.filedialog import *


def autoscroll(sbar, first, last):
    """Hide and show scrollbar as needed."""
    first, last = float(first), float(last)
    if first <= 0 and last >= 1:
        sbar.grid_remove()
    else:
        sbar.grid()
    sbar.set(first, last)


class Combobox_Autocomplete(Entry, object):
    def __init__(self, master, list_of_items=None, autocomplete_function=None, listbox_length=7, ignorecase_match=False, startswith_match=True, vscrollbar=True, hscrollbar=True, **kwargs):
        if hasattr(self, "autocomplete_function"):
            if autocomplete_function is not None:
                raise ValueError("Combobox_Autocomplete subclass has 'autocomplete_function' implemented")
        else:
            if autocomplete_function is not None:
                self.autocomplete_function = autocomplete_function
            else:
                if list_of_items is None:
                    raise ValueError("If not guiven complete function, list_of_items can't be 'None'")

                if ignorecase_match:
                    if startswith_match:
                        def matches_function(entry_data, item):
                            return item.startswith(entry_data)
                    else:
                        def matches_function(entry_data, item):
                            return item in entry_data

                    self.autocomplete_function = lambda entry_data: [item for item in self.list_of_items if matches_function(entry_data, item)]
                else:
                    if startswith_match:
                        def matches_function(escaped_entry_data, item):
                            if re.match(escaped_entry_data, item, re.IGNORECASE):
                                return True
                            else:
                                return False
                    else:
                        def matches_function(escaped_entry_data, item):
                            if re.search(escaped_entry_data, item, re.IGNORECASE):
                                return True
                            else:
                                return False
                    
                    def autocomplete_function(entry_data):
                        escaped_entry_data = re.escape(entry_data)
                        return [item for item in self.list_of_items if matches_function(escaped_entry_data, item)]

                    self.autocomplete_function = autocomplete_function

        self.listbox_length = int(listbox_length)
        self.list_of_items = list_of_items
        
        self._use_vscrollbar = vscrollbar
        self._use_hscrollbar = hscrollbar

        kwargs.setdefault("background", "white")

        if "textvariable" in kwargs:
            self._entry_var = kwargs["textvariable"]
        else:
            self._entry_var = kwargs["textvariable"] = StringVar()

        Entry.__init__(self, master, **kwargs)

        self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
        
        self.listbox = None

        self.bind("<Up>", self._on_move_up)
        self.bind("<Down>", self._on_move_down)

        self.bind("<Return>", self._update_entry_from_listbox)
        self.bind("<Escape>", lambda event: self.close_listbox())
        
    def _on_change_entry_var(self, name, index, mode):
        
        if self._entry_var.get() == '':
            self.close_listbox()
            self.focus()
        else:
            entry_data = self._entry_var.get()

            values = self.autocomplete_function(entry_data)
            if values:
                if self.listbox is None:
                    listbox_frame = Frame()

                    self.listbox = Listbox(listbox_frame, background="white", selectmode=SINGLE, activestyle="none", exportselection=False)
                    self.listbox.grid(row=0, column=0,sticky = N+E+W+S)

                    self.listbox.bind("<ButtonRelease-1>", self._update_entry_from_listbox)
                    self.listbox.bind("<Return>", self._update_entry_from_listbox)
                    self.listbox.bind("<Escape>", lambda event: self.close_listbox())

                    if self._use_vscrollbar:
                        vbar = Scrollbar(listbox_frame, orient=VERTICAL, command= self.listbox.yview)
                        vbar.grid(row=0, column=1, sticky=N+S)
                        
                        self.listbox.configure(yscrollcommand= lambda f, l: autoscroll(vbar, f, l))
                        
                    if self._use_hscrollbar:
                        hbar = Scrollbar(listbox_frame, orient=HORIZONTAL, command= self.listbox.xview)
                        hbar.grid(row=1, column=0, sticky=E+W)
                        
                        self.listbox.configure(xscrollcommand= lambda f, l: autoscroll(hbar, f, l))

                    listbox_frame.grid_columnconfigure(0, weight= 1)
                    listbox_frame.grid_rowconfigure(0, weight= 1)

                    x = -self.cget("borderwidth") - self.cget("highlightthickness") 
                    y = self.winfo_height()-self.cget("borderwidth") - self.cget("highlightthickness")

                    listbox_frame.place(in_=self, x=x, y=y, width=self.winfo_width())
                     
                else:
                    self.listbox.delete(0, END)

                height = min(self.listbox_length, len(values))
                self.listbox.configure(height=height)

                for item in values:
                    self.listbox.insert(END, item)
                
            else:
                self.close_listbox()
                self.focus()

    def close_listbox(self):
        if self.listbox is not None:
            self.listbox.master.destroy()
            self.listbox = None

    def get_value(self):
        return self._entry_var.get()

    def set_value(self, text, close_dialog=False):        
        if close_dialog:
            self._entry_var.trace_vdelete("w", self._trace_id)
            self._entry_var.set(text)
            self.close_listbox()
            self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
        else:
            self._entry_var.set(text)

        self.icursor(END)
        self.xview_moveto(1.0)

    def _update_entry_from_listbox(self, event):
        if self.listbox is not None:
            current_selection = self.listbox.curselection()
            
            if current_selection:
                self._entry_var.set(self.listbox.get(current_selection))

            self.listbox.master.destroy()
            self.listbox = None

            self.icursor(END)
            self.xview_moveto(1.0)

    def _on_move_up(self, event):
        if self.listbox is not None:
            current_selection = self.listbox.curselection()

            if len(current_selection)==0:
                self.listbox.selection_set(0)
                self.listbox.activate(0)
            else:
                index = int(current_selection[0])
                self.listbox.selection_clear(index)

                if index == 0:
                    index = END
                else:
                    index -= 1

                self.listbox.see(index)
                self.listbox.selection_set(first=index)
                self.listbox.activate(index)

    def _on_move_down(self, event):
        if self.listbox is not None:

            current_selection = self.listbox.curselection()
            if len(current_selection)==0:
                self.listbox.selection_set(0)
                self.listbox.activate(0)
            else:
                index = int(current_selection[0])
                self.listbox.selection_clear(index)
                
                if index == self.listbox.size() - 1:
                    index = 0
                else:
                    index +=1
                    
                self.listbox.see(index)
                self.listbox.selection_set(index)
                self.listbox.activate(index)

class File_Entry(Frame, object):
    def __init__(self, master, ask_dialog = askopenfilename, **dialog_options):
        Frame.__init__(self, master)
        
        self._file_autocomplete = Combobox_Autocomplete(self, autocomplete_function=self._autocomplete_function)
        self._file_autocomplete.pack(side=LEFT)

        button_size = self._file_autocomplete.winfo_reqheight()
        
        button_frame = Frame(self, height=button_size, width=button_size)
        button_frame.pack(side=LEFT, padx=(3,0))
        button_frame.pack_propagate(0)
        
        Button(button_frame, text="...", command=self._open_dialog).pack(fill=BOTH, expand=True)

        self._ask_dialog = ask_dialog

        self._dialog_options = dialog_options

    
    @property
    def path(self):
        return self._file_autocomplete.get_value()
        
    def focus(self):
        self._file_autocomplete.focus()
    
    def _open_dialog(self):
        filename = self._ask_dialog(**self._dialog_options)
        self._file_autocomplete.set_value(filename, close_dialog=True)

    def _autocomplete_function(self, base_path):
        try:
            base_path = os.path.normcase(base_path)

            if os.path.isdir(base_path):
                list_of_paths = []

                for filename in os.listdir(base_path):
                    file_path = os.path.join(base_path, filename)
                    if os.path.isdir(file_path):
                        file_path += os.sep
                    
                    list_of_paths.append(file_path)
                
                list_of_paths.sort()
                return list_of_paths
            else:
                current_directory, prefix = os.path.split(base_path)
                
                if not os.path.isdir(current_directory): return None
                
                list_of_paths = []
                for filename in os.listdir(current_directory):
                    
                    filename = os.path.normcase(filename)
                    
                    if filename.startswith(prefix):
                        file_path = os.path.join(current_directory, filename)
                        if os.path.isdir(file_path):
                            file_path += os.sep
                        list_of_paths.append(file_path)
                
                list_of_paths.sort()        
                return list_of_paths
        except os.error:
            return None

if __name__ == '__main__':
    try:
        from Tkinter import Tk
    except ImportError:
        from tkinter import Tk

    root = Tk()
    root.geometry("300x200")

    file_entry = File_Entry(root)
    file_entry.pack()
    file_entry.focus()
    
    root.mainloop()

History