# 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()