# 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("", self._on_tab) self.bind("", self._previous) self.bind("", self._next) self.bind('', self._next) self.bind('', self._previous) self.bind("", self._update_entry_from_listbox) self.bind("", 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("", self._update_entry_from_listbox) self._listbox.bind("", self._update_entry_from_listbox) self._listbox.bind("", lambda event: self.unpost_listbox()) self._listbox.bind('', self._next) self._listbox.bind('', 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()