Results of a rewrite pass
[python/dscho.git] / Lib / idlelib / IOBinding.py
blobe5ae5051aad0f2b24fd7fb05ecd2d5bbd678a29f
1 # changes by dscherer@cmu.edu
2 # - IOBinding.open() replaces the current window with the opened file,
3 # if the current window is both unmodified and unnamed
4 # - IOBinding.loadfile() interprets Windows, UNIX, and Macintosh
5 # end-of-line conventions, instead of relying on the standard library,
6 # which will only understand the local convention.
8 import os
9 import types
10 import sys
11 import codecs
12 import tempfile
13 import tkFileDialog
14 import tkMessageBox
15 import re
17 from configHandler import idleConf
19 try:
20 from codecs import BOM_UTF8
21 except ImportError:
22 # only available since Python 2.3
23 BOM_UTF8 = '\xef\xbb\xbf'
25 # Try setting the locale, so that we can find out
26 # what encoding to use
27 try:
28 import locale
29 locale.setlocale(locale.LC_CTYPE, "")
30 except ImportError:
31 pass
33 encoding = "ascii"
34 if sys.platform == 'win32':
35 # On Windows, we could use "mbcs". However, to give the user
36 # a portable encoding name, we need to find the code page
37 try:
38 encoding = locale.getdefaultlocale()[1]
39 codecs.lookup(encoding)
40 except LookupError:
41 pass
42 else:
43 try:
44 # Different things can fail here: the locale module may not be
45 # loaded, it may not offer nl_langinfo, or CODESET, or the
46 # resulting codeset may be unknown to Python. We ignore all
47 # these problems, falling back to ASCII
48 encoding = locale.nl_langinfo(locale.CODESET)
49 if encoding is None:
50 # situation occurs on Mac OS X
51 encoding = 'ascii'
52 codecs.lookup(encoding)
53 except (NameError, AttributeError, LookupError):
54 # Try getdefaultlocale well: it parses environment variables,
55 # which may give a clue. Unfortunately, getdefaultlocale has
56 # bugs that can cause ValueError.
57 try:
58 encoding = locale.getdefaultlocale()[1]
59 if encoding is None:
60 # situation occurs on Mac OS X
61 encoding = 'ascii'
62 codecs.lookup(encoding)
63 except (ValueError, LookupError):
64 pass
66 encoding = encoding.lower()
68 coding_re = re.compile("coding[:=]\s*([-\w_.]+)")
70 def coding_spec(str):
71 """Return the encoding declaration according to PEP 263.
73 Raise LookupError if the encoding is declared but unknown.
74 """
75 # Only consider the first two lines
76 str = str.split("\n")[:2]
77 str = "\n".join(str)
79 match = coding_re.search(str)
80 if not match:
81 return None
82 name = match.group(1)
83 # Check whether the encoding is known
84 import codecs
85 try:
86 codecs.lookup(name)
87 except LookupError:
88 # The standard encoding error does not indicate the encoding
89 raise LookupError, "Unknown encoding "+name
90 return name
93 class IOBinding:
95 def __init__(self, editwin):
96 self.editwin = editwin
97 self.text = editwin.text
98 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
99 self.__id_save = self.text.bind("<<save-window>>", self.save)
100 self.__id_saveas = self.text.bind("<<save-window-as-file>>",
101 self.save_as)
102 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
103 self.save_a_copy)
104 self.fileencoding = None
105 self.__id_print = self.text.bind("<<print-window>>", self.print_window)
107 def close(self):
108 # Undo command bindings
109 self.text.unbind("<<open-window-from-file>>", self.__id_open)
110 self.text.unbind("<<save-window>>", self.__id_save)
111 self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
112 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
113 self.text.unbind("<<print-window>>", self.__id_print)
114 # Break cycles
115 self.editwin = None
116 self.text = None
117 self.filename_change_hook = None
119 def get_saved(self):
120 return self.editwin.get_saved()
122 def set_saved(self, flag):
123 self.editwin.set_saved(flag)
125 def reset_undo(self):
126 self.editwin.reset_undo()
128 filename_change_hook = None
130 def set_filename_change_hook(self, hook):
131 self.filename_change_hook = hook
133 filename = None
135 def set_filename(self, filename):
136 self.filename = filename
137 self.set_saved(1)
138 if self.filename_change_hook:
139 self.filename_change_hook()
141 def open(self, event=None, editFile=None):
142 if self.editwin.flist:
143 if not editFile:
144 filename = self.askopenfile()
145 else:
146 filename=editFile
147 if filename:
148 # If the current window has no filename and hasn't been
149 # modified, we replace its contents (no loss). Otherwise
150 # we open a new window. But we won't replace the
151 # shell window (which has an interp(reter) attribute), which
152 # gets set to "not modified" at every new prompt.
153 try:
154 interp = self.editwin.interp
155 except:
156 interp = None
157 if not self.filename and self.get_saved() and not interp:
158 self.editwin.flist.open(filename, self.loadfile)
159 else:
160 self.editwin.flist.open(filename)
161 else:
162 self.text.focus_set()
163 return "break"
165 # Code for use outside IDLE:
166 if self.get_saved():
167 reply = self.maybesave()
168 if reply == "cancel":
169 self.text.focus_set()
170 return "break"
171 if not editFile:
172 filename = self.askopenfile()
173 else:
174 filename=editFile
175 if filename:
176 self.loadfile(filename)
177 else:
178 self.text.focus_set()
179 return "break"
181 def loadfile(self, filename):
182 try:
183 # open the file in binary mode so that we can handle
184 # end-of-line convention ourselves.
185 f = open(filename,'rb')
186 chars = f.read()
187 f.close()
188 except IOError, msg:
189 tkMessageBox.showerror("I/O Error", str(msg), master=self.text)
190 return False
192 chars = self.decode(chars)
193 # We now convert all end-of-lines to '\n's
194 eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac)
195 chars = re.compile( eol ).sub( r"\n", chars )
197 self.text.delete("1.0", "end")
198 self.set_filename(None)
199 self.text.insert("1.0", chars)
200 self.reset_undo()
201 self.set_filename(filename)
202 self.text.mark_set("insert", "1.0")
203 self.text.see("insert")
204 self.updaterecentfileslist(filename)
205 return True
207 def decode(self, chars):
208 """Create a Unicode string
210 If that fails, let Tcl try its best
212 # Check presence of a UTF-8 signature first
213 if chars.startswith(BOM_UTF8):
214 try:
215 chars = chars[3:].decode("utf-8")
216 except UnicodeError:
217 # has UTF-8 signature, but fails to decode...
218 return chars
219 else:
220 # Indicates that this file originally had a BOM
221 self.fileencoding = BOM_UTF8
222 return chars
223 # Next look for coding specification
224 try:
225 enc = coding_spec(chars)
226 except LookupError, name:
227 tkMessageBox.showerror(
228 title="Error loading the file",
229 message="The encoding '%s' is not known to this Python "\
230 "installation. The file may not display correctly" % name,
231 master = self.text)
232 enc = None
233 if enc:
234 try:
235 return unicode(chars, enc)
236 except UnicodeError:
237 pass
238 # If it is ASCII, we need not to record anything
239 try:
240 return unicode(chars, 'ascii')
241 except UnicodeError:
242 pass
243 # Finally, try the locale's encoding. This is deprecated;
244 # the user should declare a non-ASCII encoding
245 try:
246 chars = unicode(chars, encoding)
247 self.fileencoding = encoding
248 except UnicodeError:
249 pass
250 return chars
252 def maybesave(self):
253 if self.get_saved():
254 return "yes"
255 message = "Do you want to save %s before closing?" % (
256 self.filename or "this untitled document")
257 m = tkMessageBox.Message(
258 title="Save On Close",
259 message=message,
260 icon=tkMessageBox.QUESTION,
261 type=tkMessageBox.YESNOCANCEL,
262 master=self.text)
263 reply = m.show()
264 if reply == "yes":
265 self.save(None)
266 if not self.get_saved():
267 reply = "cancel"
268 self.text.focus_set()
269 return reply
271 def save(self, event):
272 if not self.filename:
273 self.save_as(event)
274 else:
275 if self.writefile(self.filename):
276 self.set_saved(1)
277 self.editwin.store_file_breaks()
278 self.text.focus_set()
279 return "break"
281 def save_as(self, event):
282 filename = self.asksavefile()
283 if filename:
284 if self.writefile(filename):
285 self.set_filename(filename)
286 self.set_saved(1)
287 self.editwin.store_file_breaks()
288 self.text.focus_set()
289 self.updaterecentfileslist(filename)
290 return "break"
292 def save_a_copy(self, event):
293 filename = self.asksavefile()
294 if filename:
295 self.writefile(filename)
296 self.text.focus_set()
297 self.updaterecentfileslist(filename)
298 return "break"
300 def writefile(self, filename):
301 self.fixlastline()
302 chars = self.encode(self.text.get("1.0", "end-1c"))
303 try:
304 f = open(filename, "w")
305 f.write(chars)
306 f.close()
307 return True
308 except IOError, msg:
309 tkMessageBox.showerror("I/O Error", str(msg),
310 master=self.text)
311 return False
313 def encode(self, chars):
314 if isinstance(chars, types.StringType):
315 # This is either plain ASCII, or Tk was returning mixed-encoding
316 # text to us. Don't try to guess further.
317 return chars
318 # See whether there is anything non-ASCII in it.
319 # If not, no need to figure out the encoding.
320 try:
321 return chars.encode('ascii')
322 except UnicodeError:
323 pass
324 # If there is an encoding declared, try this first.
325 try:
326 enc = coding_spec(chars)
327 failed = None
328 except LookupError, msg:
329 failed = msg
330 enc = None
331 if enc:
332 try:
333 return chars.encode(enc)
334 except UnicodeError:
335 failed = "Invalid encoding '%s'" % enc
336 if failed:
337 tkMessageBox.showerror(
338 "I/O Error",
339 "%s. Saving as UTF-8" % failed,
340 master = self.text)
341 # If there was a UTF-8 signature, use that. This should not fail
342 if self.fileencoding == BOM_UTF8 or failed:
343 return BOM_UTF8 + chars.encode("utf-8")
344 # Try the original file encoding next, if any
345 if self.fileencoding:
346 try:
347 return chars.encode(self.fileencoding)
348 except UnicodeError:
349 tkMessageBox.showerror(
350 "I/O Error",
351 "Cannot save this as '%s' anymore. Saving as UTF-8" \
352 % self.fileencoding,
353 master = self.text)
354 return BOM_UTF8 + chars.encode("utf-8")
355 # Nothing was declared, and we had not determined an encoding
356 # on loading. Recommend an encoding line.
357 try:
358 chars = chars.encode(encoding)
359 enc = encoding
360 except UnicodeError:
361 chars = BOM_UTF8 + chars.encode("utf-8")
362 enc = "utf-8"
363 tkMessageBox.showerror(
364 "I/O Error",
365 "Non-ASCII found, yet no encoding declared. Add a line like\n"
366 "# -*- coding: %s -*- \nto your file" % enc,
367 master = self.text)
368 return chars
370 def fixlastline(self):
371 c = self.text.get("end-2c")
372 if c != '\n':
373 self.text.insert("end-1c", "\n")
375 def print_window(self, event):
376 tempfilename = None
377 if self.get_saved():
378 filename = self.filename
379 else:
380 filename = tempfilename = tempfile.mktemp()
381 if not self.writefile(filename):
382 os.unlink(tempfilename)
383 return "break"
384 platform=os.name
385 printPlatform=1
386 if platform == 'posix': #posix platform
387 command = idleConf.GetOption('main','General',
388 'print-command-posix')
389 command = command + " 2>&1"
390 elif platform == 'nt': #win32 platform
391 command = idleConf.GetOption('main','General','print-command-win')
392 else: #no printing for this platform
393 printPlatform=0
394 if printPlatform: #we can try to print for this platform
395 command = command % filename
396 pipe = os.popen(command, "r")
397 output = pipe.read().strip()
398 status = pipe.close()
399 if status:
400 output = "Printing failed (exit status 0x%x)\n" % \
401 status + output
402 if output:
403 output = "Printing command: %s\n" % repr(command) + output
404 tkMessageBox.showerror("Print status", output, master=self.text)
405 else: #no printing for this platform
406 message="Printing is not enabled for this platform: %s" % platform
407 tkMessageBox.showinfo("Print status", message, master=self.text)
408 return "break"
410 opendialog = None
411 savedialog = None
413 filetypes = [
414 ("Python and text files", "*.py *.pyw *.txt", "TEXT"),
415 ("All text files", "*", "TEXT"),
416 ("All files", "*"),
419 def askopenfile(self):
420 dir, base = self.defaultfilename("open")
421 if not self.opendialog:
422 self.opendialog = tkFileDialog.Open(master=self.text,
423 filetypes=self.filetypes)
424 return self.opendialog.show(initialdir=dir, initialfile=base)
426 def defaultfilename(self, mode="open"):
427 if self.filename:
428 return os.path.split(self.filename)
429 else:
430 try:
431 pwd = os.getcwd()
432 except os.error:
433 pwd = ""
434 return pwd, ""
436 def asksavefile(self):
437 dir, base = self.defaultfilename("save")
438 if not self.savedialog:
439 self.savedialog = tkFileDialog.SaveAs(master=self.text,
440 filetypes=self.filetypes)
441 return self.savedialog.show(initialdir=dir, initialfile=base)
443 def updaterecentfileslist(self,filename):
444 "Update recent file list on all editor windows"
445 self.editwin.UpdateRecentFilesList(filename)
447 def test():
448 root = Tk()
449 class MyEditWin:
450 def __init__(self, text):
451 self.text = text
452 self.flist = None
453 self.text.bind("<Control-o>", self.open)
454 self.text.bind("<Control-s>", self.save)
455 self.text.bind("<Alt-s>", self.save_as)
456 self.text.bind("<Alt-z>", self.save_a_copy)
457 def get_saved(self): return 0
458 def set_saved(self, flag): pass
459 def reset_undo(self): pass
460 def open(self, event):
461 self.text.event_generate("<<open-window-from-file>>")
462 def save(self, event):
463 self.text.event_generate("<<save-window>>")
464 def save_as(self, event):
465 self.text.event_generate("<<save-window-as-file>>")
466 def save_a_copy(self, event):
467 self.text.event_generate("<<save-copy-of-window-as-file>>")
468 text = Text(root)
469 text.pack()
470 text.focus_set()
471 editwin = MyEditWin(text)
472 io = IOBinding(editwin)
473 root.mainloop()
475 if __name__ == "__main__":
476 from Tkinter import *
477 test()