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

import re
import os

try:
    from Tkinter import StringVar, Entry, Frame, Listbox, Button, Scrollbar
    from Tkconstants import *
except ImportError:
    from tkinter import StringVar, Entry, Frame, Listbox, Button, 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_width=None, listbox_height=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_height = int(listbox_height)
        self._listbox_width = listbox_width

        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("<Tab>", self._on_tab)
        self.bind("<Up>", self._previous)
        self.bind("<Down>", self._next)
        self.bind('<Control-n>', self._next)
        self.bind('<Control-p>', self._previous)

        self.bind("<Return>", self._update_entry_from_listbox)
        self.bind("<Escape>", lambda event: self.unpost_listbox())
        
    def _on_tab(self, event):
        self.post_listbox()
        return "break"

    def _on_change_entry_var(self, name, index, mode):
        
        entry_data = self._entry_var.get()

        if entry_data == '':
            self.unpost_listbox()
            self.focus()
        else:
            values = self.autocomplete_function(entry_data)
            if values:
                if self._listbox is None:
                    self._build_listbox(values)
                else:
                    self._listbox.delete(0, END)

                    height = min(self._listbox_height, len(values))
                    self._listbox.configure(height=height)

                    for item in values:
                        self._listbox.insert(END, item)
                
            else:
                self.unpost_listbox()
                self.focus()

    def _build_listbox(self, values):
        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.unpost_listbox())
        
        self._listbox.bind('<Control-n>', self._next)
        self._listbox.bind('<Control-p>', self._previous)

        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")

        if self._listbox_width:
            width = self._listbox_width
        else:
            width=self.winfo_width()

        listbox_frame.place(in_=self, x=x, y=y, width=width)
        
        height = min(self._listbox_height, len(values))
        self._listbox.configure(height=height)

        for item in values:
            self._listbox.insert(END, item)

    def post_listbox(self):
        if self._listbox is not None: return

        entry_data = self._entry_var.get()
        if entry_data == '': return

        values = self.autocomplete_function(entry_data)
        if values:
            self._build_listbox(values)

    def unpost_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):
        self._set_var(text)

        if close_dialog:
            self.unpost_listbox()

        self.icursor(END)
        self.xview_moveto(1.0)
        
    def _set_var(self, text):
        self._entry_var.trace_vdelete("w", self._trace_id)
        self._entry_var.set(text)
        self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)

    def _update_entry_from_listbox(self, event):
        if self._listbox is not None:
            current_selection = self._listbox.curselection()
            
            if current_selection:
                text = self._listbox.get(current_selection)
                self._set_var(text)

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

            self.focus()
            self.icursor(END)
            self.xview_moveto(1.0)
            
        return "break"

    def _previous(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)

        return "break"

    def _next(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)
        return "break"

class File_Entry(Frame, object):
    def __init__(self, master, ask_dialog = askopenfilename, width=30, **dialog_options):
        Frame.__init__(self, master)
        
        self._file_autocomplete = Combobox_Autocomplete(self, width=width, 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 base_path.endswith(os.path.sep) and 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()

Diff to Previous Revision

--- revision 8 2017-04-03 11:41:48
+++ revision 9 2017-04-03 15:18:36
@@ -1,5 +1,5 @@
 # Author: Miguel Martinez Lopez
-# Version: 0.5
+# Version: 0.6
 
 import re
 import os
@@ -67,8 +67,8 @@
 
                     self.autocomplete_function = autocomplete_function
 
-        self.listbox_height = int(listbox_height)
-        self.listbox_width = listbox_width
+        self._listbox_height = int(listbox_height)
+        self._listbox_width = listbox_width
 
         self.list_of_items = list_of_items
         
@@ -86,17 +86,19 @@
 
         self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
         
-        self.listbox = None
+        self._listbox = None
 
         self.bind("<Tab>", self._on_tab)
-        self.bind("<Up>", self._on_move_up)
-        self.bind("<Down>", self._on_move_down)
+        self.bind("<Up>", self._previous)
+        self.bind("<Down>", self._next)
+        self.bind('<Control-n>', self._next)
+        self.bind('<Control-p>', self._previous)
 
         self.bind("<Return>", self._update_entry_from_listbox)
-        self.bind("<Escape>", lambda event: self.close_listbox())
+        self.bind("<Escape>", lambda event: self.unpost_listbox())
         
     def _on_tab(self, event):
-        self.open_listbox()
+        self.post_listbox()
         return "break"
 
     def _on_change_entry_var(self, name, index, mode):
@@ -104,47 +106,50 @@
         entry_data = self._entry_var.get()
 
         if entry_data == '':
-            self.close_listbox()
+            self.unpost_listbox()
             self.focus()
         else:
             values = self.autocomplete_function(entry_data)
             if values:
-                if self.listbox is None:
+                if self._listbox is None:
                     self._build_listbox(values)
                 else:
-                    self.listbox.delete(0, END)
-
-                    height = min(self.listbox_height, len(values))
-                    self.listbox.configure(height=height)
+                    self._listbox.delete(0, END)
+
+                    height = min(self._listbox_height, len(values))
+                    self._listbox.configure(height=height)
 
                     for item in values:
-                        self.listbox.insert(END, item)
-                
-            else:
-                self.close_listbox()
+                        self._listbox.insert(END, item)
+                
+            else:
+                self.unpost_listbox()
                 self.focus()
 
     def _build_listbox(self, values):
         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())
+        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.unpost_listbox())
+        
+        self._listbox.bind('<Control-n>', self._next)
+        self._listbox.bind('<Control-p>', self._previous)
 
         if self._use_vscrollbar:
-            vbar = Scrollbar(listbox_frame, orient=VERTICAL, command= self.listbox.yview)
+            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))
+            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 = 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))
+            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)
@@ -152,21 +157,21 @@
         x = -self.cget("borderwidth") - self.cget("highlightthickness") 
         y = self.winfo_height()-self.cget("borderwidth") - self.cget("highlightthickness")
 
-        if self.listbox_width:
-            width = self.listbox_width
+        if self._listbox_width:
+            width = self._listbox_width
         else:
             width=self.winfo_width()
 
         listbox_frame.place(in_=self, x=x, y=y, width=width)
         
-        height = min(self.listbox_height, len(values))
-        self.listbox.configure(height=height)
+        height = min(self._listbox_height, len(values))
+        self._listbox.configure(height=height)
 
         for item in values:
-            self.listbox.insert(END, item)
-
-    def open_listbox(self):
-        if self.listbox is not None: return
+            self._listbox.insert(END, item)
+
+    def post_listbox(self):
+        if self._listbox is not None: return
 
         entry_data = self._entry_var.get()
         if entry_data == '': return
@@ -175,10 +180,10 @@
         if values:
             self._build_listbox(values)
 
-    def close_listbox(self):
-        if self.listbox is not None:
-            self.listbox.master.destroy()
-            self.listbox = None
+    def unpost_listbox(self):
+        if self._listbox is not None:
+            self._listbox.master.destroy()
+            self._listbox = None
 
     def get_value(self):
         return self._entry_var.get()
@@ -187,7 +192,7 @@
         self._set_var(text)
 
         if close_dialog:
-            self.close_listbox()
+            self.unpost_listbox()
 
         self.icursor(END)
         self.xview_moveto(1.0)
@@ -198,15 +203,15 @@
         self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)
 
     def _update_entry_from_listbox(self, event):
-        if self.listbox is not None:
-            current_selection = self.listbox.curselection()
+        if self._listbox is not None:
+            current_selection = self._listbox.curselection()
             
             if current_selection:
-                text = self.listbox.get(current_selection)
+                text = self._listbox.get(current_selection)
                 self._set_var(text)
 
-            self.listbox.master.destroy()
-            self.listbox = None
+            self._listbox.master.destroy()
+            self._listbox = None
 
             self.focus()
             self.icursor(END)
@@ -214,47 +219,47 @@
             
         return "break"
 
-    def _on_move_up(self, event):
-        if self.listbox is not None:
-            current_selection = self.listbox.curselection()
+    def _previous(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)
+                self._listbox.selection_set(0)
+                self._listbox.activate(0)
             else:
                 index = int(current_selection[0])
-                self.listbox.selection_clear(index)
+                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)
+                self._listbox.see(index)
+                self._listbox.selection_set(first=index)
+                self._listbox.activate(index)
 
         return "break"
 
-    def _on_move_down(self, event):
-        if self.listbox is not None:
-
-            current_selection = self.listbox.curselection()
+    def _next(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)
+                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:
+                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)
+                self._listbox.see(index)
+                self._listbox.selection_set(index)
+                self._listbox.activate(index)
         return "break"
 
 class File_Entry(Frame, object):

History