bump to 3.1.3rc1
[python/dscho.git] / Lib / idlelib / IOBinding.py
blob1b7bfc1da9c418fc31e1f17e6e8b32a986180909
1 import os
2 import types
3 import sys
4 import codecs
5 import tempfile
6 import tkinter.filedialog as tkFileDialog
7 import tkinter.messagebox as tkMessageBox
8 import re
9 from tkinter import *
10 from tkinter.simpledialog import askstring
12 from idlelib.configHandler import idleConf
14 from codecs import BOM_UTF8
16 # Try setting the locale, so that we can find out
17 # what encoding to use
18 try:
19 import locale
20 locale.setlocale(locale.LC_CTYPE, "")
21 except (ImportError, locale.Error):
22 pass
24 # Encoding for file names
25 filesystemencoding = sys.getfilesystemencoding() ### currently unused
27 locale_encoding = 'ascii'
28 if sys.platform == 'win32':
29 # On Windows, we could use "mbcs". However, to give the user
30 # a portable encoding name, we need to find the code page
31 try:
32 locale_encoding = locale.getdefaultlocale()[1]
33 codecs.lookup(locale_encoding)
34 except LookupError:
35 pass
36 else:
37 try:
38 # Different things can fail here: the locale module may not be
39 # loaded, it may not offer nl_langinfo, or CODESET, or the
40 # resulting codeset may be unknown to Python. We ignore all
41 # these problems, falling back to ASCII
42 locale_encoding = locale.nl_langinfo(locale.CODESET)
43 if locale_encoding is None or locale_encoding is '':
44 # situation occurs on Mac OS X
45 locale_encoding = 'ascii'
46 codecs.lookup(locale_encoding)
47 except (NameError, AttributeError, LookupError):
48 # Try getdefaultlocale: it parses environment variables,
49 # which may give a clue. Unfortunately, getdefaultlocale has
50 # bugs that can cause ValueError.
51 try:
52 locale_encoding = locale.getdefaultlocale()[1]
53 if locale_encoding is None or locale_encoding is '':
54 # situation occurs on Mac OS X
55 locale_encoding = 'ascii'
56 codecs.lookup(locale_encoding)
57 except (ValueError, LookupError):
58 pass
60 locale_encoding = locale_encoding.lower()
62 encoding = locale_encoding ### KBK 07Sep07 This is used all over IDLE, check!
63 ### 'encoding' is used below in encode(), check!
65 coding_re = re.compile("coding[:=]\s*([-\w_.]+)")
67 def coding_spec(data):
68 """Return the encoding declaration according to PEP 263.
70 When checking encoded data, only the first two lines should be passed
71 in to avoid a UnicodeDecodeError if the rest of the data is not unicode.
72 The first two lines would contain the encoding specification.
74 Raise a LookupError if the encoding is declared but unknown.
75 """
76 if isinstance(data, bytes):
77 # This encoding might be wrong. However, the coding
78 # spec must be ASCII-only, so any non-ASCII characters
79 # around here will be ignored. Decoding to Latin-1 should
80 # never fail (except for memory outage)
81 lines = data.decode('iso-8859-1')
82 else:
83 lines = data
84 # consider only the first two lines
85 if '\n' in lines:
86 lst = lines.split('\n')[:2]
87 elif '\r' in lines:
88 lst = lines.split('\r')[:2]
89 else:
90 lst = list(lines)
91 str = '\n'.join(lst)
92 match = coding_re.search(str)
93 if not match:
94 return None
95 name = match.group(1)
96 try:
97 codecs.lookup(name)
98 except LookupError:
99 # The standard encoding error does not indicate the encoding
100 raise LookupError("Unknown encoding: "+name)
101 return name
104 class IOBinding:
106 def __init__(self, editwin):
107 self.editwin = editwin
108 self.text = editwin.text
109 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
110 self.__id_save = self.text.bind("<<save-window>>", self.save)
111 self.__id_saveas = self.text.bind("<<save-window-as-file>>",
112 self.save_as)
113 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
114 self.save_a_copy)
115 self.fileencoding = None
116 self.__id_print = self.text.bind("<<print-window>>", self.print_window)
118 def close(self):
119 # Undo command bindings
120 self.text.unbind("<<open-window-from-file>>", self.__id_open)
121 self.text.unbind("<<save-window>>", self.__id_save)
122 self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
123 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
124 self.text.unbind("<<print-window>>", self.__id_print)
125 # Break cycles
126 self.editwin = None
127 self.text = None
128 self.filename_change_hook = None
130 def get_saved(self):
131 return self.editwin.get_saved()
133 def set_saved(self, flag):
134 self.editwin.set_saved(flag)
136 def reset_undo(self):
137 self.editwin.reset_undo()
139 filename_change_hook = None
141 def set_filename_change_hook(self, hook):
142 self.filename_change_hook = hook
144 filename = None
145 dirname = None
147 def set_filename(self, filename):
148 if filename and os.path.isdir(filename):
149 self.filename = None
150 self.dirname = filename
151 else:
152 self.filename = filename
153 self.dirname = None
154 self.set_saved(1)
155 if self.filename_change_hook:
156 self.filename_change_hook()
158 def open(self, event=None, editFile=None):
159 if self.editwin.flist:
160 if not editFile:
161 filename = self.askopenfile()
162 else:
163 filename=editFile
164 if filename:
165 # If the current window has no filename and hasn't been
166 # modified, we replace its contents (no loss). Otherwise
167 # we open a new window. But we won't replace the
168 # shell window (which has an interp(reter) attribute), which
169 # gets set to "not modified" at every new prompt.
170 try:
171 interp = self.editwin.interp
172 except AttributeError:
173 interp = None
174 if not self.filename and self.get_saved() and not interp:
175 self.editwin.flist.open(filename, self.loadfile)
176 else:
177 self.editwin.flist.open(filename)
178 else:
179 self.text.focus_set()
180 return "break"
182 # Code for use outside IDLE:
183 if self.get_saved():
184 reply = self.maybesave()
185 if reply == "cancel":
186 self.text.focus_set()
187 return "break"
188 if not editFile:
189 filename = self.askopenfile()
190 else:
191 filename=editFile
192 if filename:
193 self.loadfile(filename)
194 else:
195 self.text.focus_set()
196 return "break"
198 eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac)
199 eol_re = re.compile(eol)
200 eol_convention = os.linesep # default
202 def loadfile(self, filename):
203 try:
204 # open the file in binary mode so that we can handle
205 # end-of-line convention ourselves.
206 f = open(filename,'rb')
207 two_lines = f.readline() + f.readline()
208 f.seek(0)
209 bytes = f.read()
210 f.close()
211 except IOError as msg:
212 tkMessageBox.showerror("I/O Error", str(msg), master=self.text)
213 return False
214 chars, converted = self._decode(two_lines, bytes)
215 if chars is None:
216 tkMessageBox.showerror("Decoding Error",
217 "File %s\nFailed to Decode" % filename,
218 parent=self.text)
219 return False
220 # We now convert all end-of-lines to '\n's
221 firsteol = self.eol_re.search(chars)
222 if firsteol:
223 self.eol_convention = firsteol.group(0)
224 chars = self.eol_re.sub(r"\n", chars)
225 self.text.delete("1.0", "end")
226 self.set_filename(None)
227 self.text.insert("1.0", chars)
228 self.reset_undo()
229 self.set_filename(filename)
230 if converted:
231 # We need to save the conversion results first
232 # before being able to execute the code
233 self.set_saved(False)
234 self.text.mark_set("insert", "1.0")
235 self.text.see("insert")
236 self.updaterecentfileslist(filename)
237 return True
239 def _decode(self, two_lines, bytes):
240 "Create a Unicode string."
241 chars = None
242 # Check presence of a UTF-8 signature first
243 if bytes.startswith(BOM_UTF8):
244 try:
245 chars = bytes[3:].decode("utf-8")
246 except UnicodeDecodeError:
247 # has UTF-8 signature, but fails to decode...
248 return None, False
249 else:
250 # Indicates that this file originally had a BOM
251 self.fileencoding = 'BOM'
252 return chars, False
253 # Next look for coding specification
254 try:
255 enc = coding_spec(two_lines)
256 except LookupError as name:
257 tkMessageBox.showerror(
258 title="Error loading the file",
259 message="The encoding '%s' is not known to this Python "\
260 "installation. The file may not display correctly" % name,
261 master = self.text)
262 enc = None
263 except UnicodeDecodeError:
264 return None, False
265 if enc:
266 try:
267 chars = str(bytes, enc)
268 self.fileencoding = enc
269 return chars, False
270 except UnicodeDecodeError:
271 pass
272 # Try ascii:
273 try:
274 chars = str(bytes, 'ascii')
275 self.fileencoding = None
276 return chars, False
277 except UnicodeDecodeError:
278 pass
279 # Try utf-8:
280 try:
281 chars = str(bytes, 'utf-8')
282 self.fileencoding = 'utf-8'
283 return chars, False
284 except UnicodeDecodeError:
285 pass
286 # Finally, try the locale's encoding. This is deprecated;
287 # the user should declare a non-ASCII encoding
288 try:
289 # Wait for the editor window to appear
290 self.editwin.text.update()
291 enc = askstring(
292 "Specify file encoding",
293 "The file's encoding is invalid for Python 3.x.\n"
294 "IDLE will convert it to UTF-8.\n"
295 "What is the current encoding of the file?",
296 initialvalue = locale_encoding,
297 parent = self.editwin.text)
299 if enc:
300 chars = str(bytes, enc)
301 self.fileencoding = None
302 return chars, True
303 except (UnicodeDecodeError, LookupError):
304 pass
305 return None, False # None on failure
307 def maybesave(self):
308 if self.get_saved():
309 return "yes"
310 message = "Do you want to save %s before closing?" % (
311 self.filename or "this untitled document")
312 m = tkMessageBox.Message(
313 title="Save On Close",
314 message=message,
315 icon=tkMessageBox.QUESTION,
316 type=tkMessageBox.YESNOCANCEL,
317 master=self.text)
318 reply = m.show()
319 if reply == "yes":
320 self.save(None)
321 if not self.get_saved():
322 reply = "cancel"
323 self.text.focus_set()
324 return reply
326 def save(self, event):
327 if not self.filename:
328 self.save_as(event)
329 else:
330 if self.writefile(self.filename):
331 self.set_saved(1)
332 try:
333 self.editwin.store_file_breaks()
334 except AttributeError: # may be a PyShell
335 pass
336 self.text.focus_set()
337 return "break"
339 def save_as(self, event):
340 filename = self.asksavefile()
341 if filename:
342 if self.writefile(filename):
343 self.set_filename(filename)
344 self.set_saved(1)
345 try:
346 self.editwin.store_file_breaks()
347 except AttributeError:
348 pass
349 self.text.focus_set()
350 self.updaterecentfileslist(filename)
351 return "break"
353 def save_a_copy(self, event):
354 filename = self.asksavefile()
355 if filename:
356 self.writefile(filename)
357 self.text.focus_set()
358 self.updaterecentfileslist(filename)
359 return "break"
361 def writefile(self, filename):
362 self.fixlastline()
363 text = self.text.get("1.0", "end-1c")
364 if self.eol_convention != "\n":
365 text = text.replace("\n", self.eol_convention)
366 chars = self.encode(text)
367 try:
368 f = open(filename, "wb")
369 f.write(chars)
370 f.flush()
371 f.close()
372 return True
373 except IOError as msg:
374 tkMessageBox.showerror("I/O Error", str(msg),
375 master=self.text)
376 return False
378 def encode(self, chars):
379 if isinstance(chars, bytes):
380 # This is either plain ASCII, or Tk was returning mixed-encoding
381 # text to us. Don't try to guess further.
382 return chars
383 # Preserve a BOM that might have been present on opening
384 if self.fileencoding == 'BOM':
385 return BOM_UTF8 + chars.encode("utf-8")
386 # See whether there is anything non-ASCII in it.
387 # If not, no need to figure out the encoding.
388 try:
389 return chars.encode('ascii')
390 except UnicodeError:
391 pass
392 # Check if there is an encoding declared
393 try:
394 # a string, let coding_spec slice it to the first two lines
395 enc = coding_spec(chars)
396 failed = None
397 except LookupError as msg:
398 failed = msg
399 enc = None
400 else:
401 if not enc:
402 # PEP 3120: default source encoding is UTF-8
403 enc = 'utf-8'
404 if enc:
405 try:
406 return chars.encode(enc)
407 except UnicodeError:
408 failed = "Invalid encoding '%s'" % enc
409 tkMessageBox.showerror(
410 "I/O Error",
411 "%s.\nSaving as UTF-8" % failed,
412 master = self.text)
413 # Fallback: save as UTF-8, with BOM - ignoring the incorrect
414 # declared encoding
415 return BOM_UTF8 + chars.encode("utf-8")
417 def fixlastline(self):
418 c = self.text.get("end-2c")
419 if c != '\n':
420 self.text.insert("end-1c", "\n")
422 def print_window(self, event):
423 m = tkMessageBox.Message(
424 title="Print",
425 message="Print to Default Printer",
426 icon=tkMessageBox.QUESTION,
427 type=tkMessageBox.OKCANCEL,
428 default=tkMessageBox.OK,
429 master=self.text)
430 reply = m.show()
431 if reply != tkMessageBox.OK:
432 self.text.focus_set()
433 return "break"
434 tempfilename = None
435 saved = self.get_saved()
436 if saved:
437 filename = self.filename
438 # shell undo is reset after every prompt, looks saved, probably isn't
439 if not saved or filename is None:
440 (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
441 filename = tempfilename
442 os.close(tfd)
443 if not self.writefile(tempfilename):
444 os.unlink(tempfilename)
445 return "break"
446 platform=os.name
447 printPlatform=1
448 if platform == 'posix': #posix platform
449 command = idleConf.GetOption('main','General',
450 'print-command-posix')
451 command = command + " 2>&1"
452 elif platform == 'nt': #win32 platform
453 command = idleConf.GetOption('main','General','print-command-win')
454 else: #no printing for this platform
455 printPlatform=0
456 if printPlatform: #we can try to print for this platform
457 command = command % filename
458 pipe = os.popen(command, "r")
459 # things can get ugly on NT if there is no printer available.
460 output = pipe.read().strip()
461 status = pipe.close()
462 if status:
463 output = "Printing failed (exit status 0x%x)\n" % \
464 status + output
465 if output:
466 output = "Printing command: %s\n" % repr(command) + output
467 tkMessageBox.showerror("Print status", output, master=self.text)
468 else: #no printing for this platform
469 message="Printing is not enabled for this platform: %s" % platform
470 tkMessageBox.showinfo("Print status", message, master=self.text)
471 if tempfilename:
472 os.unlink(tempfilename)
473 return "break"
475 opendialog = None
476 savedialog = None
478 filetypes = [
479 ("Python and text files", "*.py *.pyw *.txt", "TEXT"),
480 ("All text files", "*", "TEXT"),
481 ("All files", "*"),
484 def askopenfile(self):
485 dir, base = self.defaultfilename("open")
486 if not self.opendialog:
487 self.opendialog = tkFileDialog.Open(master=self.text,
488 filetypes=self.filetypes)
489 filename = self.opendialog.show(initialdir=dir, initialfile=base)
490 return filename
492 def defaultfilename(self, mode="open"):
493 if self.filename:
494 return os.path.split(self.filename)
495 elif self.dirname:
496 return self.dirname, ""
497 else:
498 try:
499 pwd = os.getcwd()
500 except os.error:
501 pwd = ""
502 return pwd, ""
504 def asksavefile(self):
505 dir, base = self.defaultfilename("save")
506 if not self.savedialog:
507 self.savedialog = tkFileDialog.SaveAs(master=self.text,
508 filetypes=self.filetypes)
509 filename = self.savedialog.show(initialdir=dir, initialfile=base)
510 return filename
512 def updaterecentfileslist(self,filename):
513 "Update recent file list on all editor windows"
514 if self.editwin.flist:
515 self.editwin.update_recent_files_list(filename)
517 def test():
518 root = Tk()
519 class MyEditWin:
520 def __init__(self, text):
521 self.text = text
522 self.flist = None
523 self.text.bind("<Control-o>", self.open)
524 self.text.bind("<Control-s>", self.save)
525 self.text.bind("<Alt-s>", self.save_as)
526 self.text.bind("<Alt-z>", self.save_a_copy)
527 def get_saved(self): return 0
528 def set_saved(self, flag): pass
529 def reset_undo(self): pass
530 def open(self, event):
531 self.text.event_generate("<<open-window-from-file>>")
532 def save(self, event):
533 self.text.event_generate("<<save-window>>")
534 def save_as(self, event):
535 self.text.event_generate("<<save-window-as-file>>")
536 def save_a_copy(self, event):
537 self.text.event_generate("<<save-copy-of-window-as-file>>")
538 text = Text(root)
539 text.pack()
540 text.focus_set()
541 editwin = MyEditWin(text)
542 io = IOBinding(editwin)
543 root.mainloop()
545 if __name__ == "__main__":
546 test()