1"""File selection dialog classes.
2
3Classes:
4
5- FileDialog
6- LoadFileDialog
7- SaveFileDialog
8
9"""
10
11from Tkinter import *
12from Dialog import Dialog
13
14import os
15import fnmatch
16
17
18dialogstates = {}
19
20
21class FileDialog:
22
23    """Standard file selection dialog -- no checks on selected file.
24
25    Usage:
26
27        d = FileDialog(master)
28        fname = d.go(dir_or_file, pattern, default, key)
29        if fname is None: ...canceled...
30        else: ...open file...
31
32    All arguments to go() are optional.
33
34    The 'key' argument specifies a key in the global dictionary
35    'dialogstates', which keeps track of the values for the directory
36    and pattern arguments, overriding the values passed in (it does
37    not keep track of the default argument!).  If no key is specified,
38    the dialog keeps no memory of previous state.  Note that memory is
39    kept even when the dialog is canceled.  (All this emulates the
40    behavior of the Macintosh file selection dialogs.)
41
42    """
43
44    title = "File Selection Dialog"
45
46    def __init__(self, master, title=None):
47        if title is None: title = self.title
48        self.master = master
49        self.directory = None
50
51        self.top = Toplevel(master)
52        self.top.title(title)
53        self.top.iconname(title)
54
55        self.botframe = Frame(self.top)
56        self.botframe.pack(side=BOTTOM, fill=X)
57
58        self.selection = Entry(self.top)
59        self.selection.pack(side=BOTTOM, fill=X)
60        self.selection.bind('<Return>', self.ok_event)
61
62        self.filter = Entry(self.top)
63        self.filter.pack(side=TOP, fill=X)
64        self.filter.bind('<Return>', self.filter_command)
65
66        self.midframe = Frame(self.top)
67        self.midframe.pack(expand=YES, fill=BOTH)
68
69        self.filesbar = Scrollbar(self.midframe)
70        self.filesbar.pack(side=RIGHT, fill=Y)
71        self.files = Listbox(self.midframe, exportselection=0,
72                             yscrollcommand=(self.filesbar, 'set'))
73        self.files.pack(side=RIGHT, expand=YES, fill=BOTH)
74        btags = self.files.bindtags()
75        self.files.bindtags(btags[1:] + btags[:1])
76        self.files.bind('<ButtonRelease-1>', self.files_select_event)
77        self.files.bind('<Double-ButtonRelease-1>', self.files_double_event)
78        self.filesbar.config(command=(self.files, 'yview'))
79
80        self.dirsbar = Scrollbar(self.midframe)
81        self.dirsbar.pack(side=LEFT, fill=Y)
82        self.dirs = Listbox(self.midframe, exportselection=0,
83                            yscrollcommand=(self.dirsbar, 'set'))
84        self.dirs.pack(side=LEFT, expand=YES, fill=BOTH)
85        self.dirsbar.config(command=(self.dirs, 'yview'))
86        btags = self.dirs.bindtags()
87        self.dirs.bindtags(btags[1:] + btags[:1])
88        self.dirs.bind('<ButtonRelease-1>', self.dirs_select_event)
89        self.dirs.bind('<Double-ButtonRelease-1>', self.dirs_double_event)
90
91        self.ok_button = Button(self.botframe,
92                                 text="OK",
93                                 command=self.ok_command)
94        self.ok_button.pack(side=LEFT)
95        self.filter_button = Button(self.botframe,
96                                    text="Filter",
97                                    command=self.filter_command)
98        self.filter_button.pack(side=LEFT, expand=YES)
99        self.cancel_button = Button(self.botframe,
100                                    text="Cancel",
101                                    command=self.cancel_command)
102        self.cancel_button.pack(side=RIGHT)
103
104        self.top.protocol('WM_DELETE_WINDOW', self.cancel_command)
105        # XXX Are the following okay for a general audience?
106        self.top.bind('<Alt-w>', self.cancel_command)
107        self.top.bind('<Alt-W>', self.cancel_command)
108
109    def go(self, dir_or_file=os.curdir, pattern="*", default="", key=None):
110        if key and key in dialogstates:
111            self.directory, pattern = dialogstates[key]
112        else:
113            dir_or_file = os.path.expanduser(dir_or_file)
114            if os.path.isdir(dir_or_file):
115                self.directory = dir_or_file
116            else:
117                self.directory, default = os.path.split(dir_or_file)
118        self.set_filter(self.directory, pattern)
119        self.set_selection(default)
120        self.filter_command()
121        self.selection.focus_set()
122        self.top.wait_visibility() # window needs to be visible for the grab
123        self.top.grab_set()
124        self.how = None
125        self.master.mainloop()          # Exited by self.quit(how)
126        if key:
127            directory, pattern = self.get_filter()
128            if self.how:
129                directory = os.path.dirname(self.how)
130            dialogstates[key] = directory, pattern
131        self.top.destroy()
132        return self.how
133
134    def quit(self, how=None):
135        self.how = how
136        self.master.quit()              # Exit mainloop()
137
138    def dirs_double_event(self, event):
139        self.filter_command()
140
141    def dirs_select_event(self, event):
142        dir, pat = self.get_filter()
143        subdir = self.dirs.get('active')
144        dir = os.path.normpath(os.path.join(self.directory, subdir))
145        self.set_filter(dir, pat)
146
147    def files_double_event(self, event):
148        self.ok_command()
149
150    def files_select_event(self, event):
151        file = self.files.get('active')
152        self.set_selection(file)
153
154    def ok_event(self, event):
155        self.ok_command()
156
157    def ok_command(self):
158        self.quit(self.get_selection())
159
160    def filter_command(self, event=None):
161        dir, pat = self.get_filter()
162        try:
163            names = os.listdir(dir)
164        except os.error:
165            self.master.bell()
166            return
167        self.directory = dir
168        self.set_filter(dir, pat)
169        names.sort()
170        subdirs = [os.pardir]
171        matchingfiles = []
172        for name in names:
173            fullname = os.path.join(dir, name)
174            if os.path.isdir(fullname):
175                subdirs.append(name)
176            elif fnmatch.fnmatch(name, pat):
177                matchingfiles.append(name)
178        self.dirs.delete(0, END)
179        for name in subdirs:
180            self.dirs.insert(END, name)
181        self.files.delete(0, END)
182        for name in matchingfiles:
183            self.files.insert(END, name)
184        head, tail = os.path.split(self.get_selection())
185        if tail == os.curdir: tail = ''
186        self.set_selection(tail)
187
188    def get_filter(self):
189        filter = self.filter.get()
190        filter = os.path.expanduser(filter)
191        if filter[-1:] == os.sep or os.path.isdir(filter):
192            filter = os.path.join(filter, "*")
193        return os.path.split(filter)
194
195    def get_selection(self):
196        file = self.selection.get()
197        file = os.path.expanduser(file)
198        return file
199
200    def cancel_command(self, event=None):
201        self.quit()
202
203    def set_filter(self, dir, pat):
204        if not os.path.isabs(dir):
205            try:
206                pwd = os.getcwd()
207            except os.error:
208                pwd = None
209            if pwd:
210                dir = os.path.join(pwd, dir)
211                dir = os.path.normpath(dir)
212        self.filter.delete(0, END)
213        self.filter.insert(END, os.path.join(dir or os.curdir, pat or "*"))
214
215    def set_selection(self, file):
216        self.selection.delete(0, END)
217        self.selection.insert(END, os.path.join(self.directory, file))
218
219
220class LoadFileDialog(FileDialog):
221
222    """File selection dialog which checks that the file exists."""
223
224    title = "Load File Selection Dialog"
225
226    def ok_command(self):
227        file = self.get_selection()
228        if not os.path.isfile(file):
229            self.master.bell()
230        else:
231            self.quit(file)
232
233
234class SaveFileDialog(FileDialog):
235
236    """File selection dialog which checks that the file may be created."""
237
238    title = "Save File Selection Dialog"
239
240    def ok_command(self):
241        file = self.get_selection()
242        if os.path.exists(file):
243            if os.path.isdir(file):
244                self.master.bell()
245                return
246            d = Dialog(self.top,
247                       title="Overwrite Existing File Question",
248                       text="Overwrite existing file %r?" % (file,),
249                       bitmap='questhead',
250                       default=1,
251                       strings=("Yes", "Cancel"))
252            if d.num != 0:
253                return
254        else:
255            head, tail = os.path.split(file)
256            if not os.path.isdir(head):
257                self.master.bell()
258                return
259        self.quit(file)
260
261
262def test():
263    """Simple test program."""
264    root = Tk()
265    root.withdraw()
266    fd = LoadFileDialog(root)
267    loadfile = fd.go(key="test")
268    fd = SaveFileDialog(root)
269    savefile = fd.go(key="test")
270    print loadfile, savefile
271
272
273if __name__ == '__main__':
274    test()
275