|
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. |
|
7 |
|
8 import os |
|
9 import types |
|
10 import sys |
|
11 import codecs |
|
12 import tempfile |
|
13 import tkFileDialog |
|
14 import tkMessageBox |
|
15 import re |
|
16 from Tkinter import * |
|
17 from SimpleDialog import SimpleDialog |
|
18 |
|
19 from configHandler import idleConf |
|
20 |
|
21 try: |
|
22 from codecs import BOM_UTF8 |
|
23 except ImportError: |
|
24 # only available since Python 2.3 |
|
25 BOM_UTF8 = '\xef\xbb\xbf' |
|
26 |
|
27 # Try setting the locale, so that we can find out |
|
28 # what encoding to use |
|
29 try: |
|
30 import locale |
|
31 locale.setlocale(locale.LC_CTYPE, "") |
|
32 except (ImportError, locale.Error): |
|
33 pass |
|
34 |
|
35 # Encoding for file names |
|
36 filesystemencoding = sys.getfilesystemencoding() |
|
37 |
|
38 encoding = "ascii" |
|
39 if sys.platform == 'win32': |
|
40 # On Windows, we could use "mbcs". However, to give the user |
|
41 # a portable encoding name, we need to find the code page |
|
42 try: |
|
43 encoding = locale.getdefaultlocale()[1] |
|
44 codecs.lookup(encoding) |
|
45 except LookupError: |
|
46 pass |
|
47 else: |
|
48 try: |
|
49 # Different things can fail here: the locale module may not be |
|
50 # loaded, it may not offer nl_langinfo, or CODESET, or the |
|
51 # resulting codeset may be unknown to Python. We ignore all |
|
52 # these problems, falling back to ASCII |
|
53 encoding = locale.nl_langinfo(locale.CODESET) |
|
54 if encoding is None or encoding is '': |
|
55 # situation occurs on Mac OS X |
|
56 encoding = 'ascii' |
|
57 codecs.lookup(encoding) |
|
58 except (NameError, AttributeError, LookupError): |
|
59 # Try getdefaultlocale well: it parses environment variables, |
|
60 # which may give a clue. Unfortunately, getdefaultlocale has |
|
61 # bugs that can cause ValueError. |
|
62 try: |
|
63 encoding = locale.getdefaultlocale()[1] |
|
64 if encoding is None or encoding is '': |
|
65 # situation occurs on Mac OS X |
|
66 encoding = 'ascii' |
|
67 codecs.lookup(encoding) |
|
68 except (ValueError, LookupError): |
|
69 pass |
|
70 |
|
71 encoding = encoding.lower() |
|
72 |
|
73 coding_re = re.compile("coding[:=]\s*([-\w_.]+)") |
|
74 |
|
75 class EncodingMessage(SimpleDialog): |
|
76 "Inform user that an encoding declaration is needed." |
|
77 def __init__(self, master, enc): |
|
78 self.should_edit = False |
|
79 |
|
80 self.root = top = Toplevel(master) |
|
81 top.bind("<Return>", self.return_event) |
|
82 top.bind("<Escape>", self.do_ok) |
|
83 top.protocol("WM_DELETE_WINDOW", self.wm_delete_window) |
|
84 top.wm_title("I/O Warning") |
|
85 top.wm_iconname("I/O Warning") |
|
86 self.top = top |
|
87 |
|
88 l1 = Label(top, |
|
89 text="Non-ASCII found, yet no encoding declared. Add a line like") |
|
90 l1.pack(side=TOP, anchor=W) |
|
91 l2 = Entry(top, font="courier") |
|
92 l2.insert(0, "# -*- coding: %s -*-" % enc) |
|
93 # For some reason, the text is not selectable anymore if the |
|
94 # widget is disabled. |
|
95 # l2['state'] = DISABLED |
|
96 l2.pack(side=TOP, anchor = W, fill=X) |
|
97 l3 = Label(top, text="to your file\n" |
|
98 "Choose OK to save this file as %s\n" |
|
99 "Edit your general options to silence this warning" % enc) |
|
100 l3.pack(side=TOP, anchor = W) |
|
101 |
|
102 buttons = Frame(top) |
|
103 buttons.pack(side=TOP, fill=X) |
|
104 # Both return and cancel mean the same thing: do nothing |
|
105 self.default = self.cancel = 0 |
|
106 b1 = Button(buttons, text="Ok", default="active", |
|
107 command=self.do_ok) |
|
108 b1.pack(side=LEFT, fill=BOTH, expand=1) |
|
109 b2 = Button(buttons, text="Edit my file", |
|
110 command=self.do_edit) |
|
111 b2.pack(side=LEFT, fill=BOTH, expand=1) |
|
112 |
|
113 self._set_transient(master) |
|
114 |
|
115 def do_ok(self): |
|
116 self.done(0) |
|
117 |
|
118 def do_edit(self): |
|
119 self.done(1) |
|
120 |
|
121 def coding_spec(str): |
|
122 """Return the encoding declaration according to PEP 263. |
|
123 |
|
124 Raise LookupError if the encoding is declared but unknown. |
|
125 """ |
|
126 # Only consider the first two lines |
|
127 str = str.split("\n")[:2] |
|
128 str = "\n".join(str) |
|
129 |
|
130 match = coding_re.search(str) |
|
131 if not match: |
|
132 return None |
|
133 name = match.group(1) |
|
134 # Check whether the encoding is known |
|
135 import codecs |
|
136 try: |
|
137 codecs.lookup(name) |
|
138 except LookupError: |
|
139 # The standard encoding error does not indicate the encoding |
|
140 raise LookupError, "Unknown encoding "+name |
|
141 return name |
|
142 |
|
143 |
|
144 class IOBinding: |
|
145 |
|
146 def __init__(self, editwin): |
|
147 self.editwin = editwin |
|
148 self.text = editwin.text |
|
149 self.__id_open = self.text.bind("<<open-window-from-file>>", self.open) |
|
150 self.__id_save = self.text.bind("<<save-window>>", self.save) |
|
151 self.__id_saveas = self.text.bind("<<save-window-as-file>>", |
|
152 self.save_as) |
|
153 self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>", |
|
154 self.save_a_copy) |
|
155 self.fileencoding = None |
|
156 self.__id_print = self.text.bind("<<print-window>>", self.print_window) |
|
157 |
|
158 def close(self): |
|
159 # Undo command bindings |
|
160 self.text.unbind("<<open-window-from-file>>", self.__id_open) |
|
161 self.text.unbind("<<save-window>>", self.__id_save) |
|
162 self.text.unbind("<<save-window-as-file>>",self.__id_saveas) |
|
163 self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy) |
|
164 self.text.unbind("<<print-window>>", self.__id_print) |
|
165 # Break cycles |
|
166 self.editwin = None |
|
167 self.text = None |
|
168 self.filename_change_hook = None |
|
169 |
|
170 def get_saved(self): |
|
171 return self.editwin.get_saved() |
|
172 |
|
173 def set_saved(self, flag): |
|
174 self.editwin.set_saved(flag) |
|
175 |
|
176 def reset_undo(self): |
|
177 self.editwin.reset_undo() |
|
178 |
|
179 filename_change_hook = None |
|
180 |
|
181 def set_filename_change_hook(self, hook): |
|
182 self.filename_change_hook = hook |
|
183 |
|
184 filename = None |
|
185 dirname = None |
|
186 |
|
187 def set_filename(self, filename): |
|
188 if filename and os.path.isdir(filename): |
|
189 self.filename = None |
|
190 self.dirname = filename |
|
191 else: |
|
192 self.filename = filename |
|
193 self.dirname = None |
|
194 self.set_saved(1) |
|
195 if self.filename_change_hook: |
|
196 self.filename_change_hook() |
|
197 |
|
198 def open(self, event=None, editFile=None): |
|
199 if self.editwin.flist: |
|
200 if not editFile: |
|
201 filename = self.askopenfile() |
|
202 else: |
|
203 filename=editFile |
|
204 if filename: |
|
205 # If the current window has no filename and hasn't been |
|
206 # modified, we replace its contents (no loss). Otherwise |
|
207 # we open a new window. But we won't replace the |
|
208 # shell window (which has an interp(reter) attribute), which |
|
209 # gets set to "not modified" at every new prompt. |
|
210 try: |
|
211 interp = self.editwin.interp |
|
212 except AttributeError: |
|
213 interp = None |
|
214 if not self.filename and self.get_saved() and not interp: |
|
215 self.editwin.flist.open(filename, self.loadfile) |
|
216 else: |
|
217 self.editwin.flist.open(filename) |
|
218 else: |
|
219 self.text.focus_set() |
|
220 return "break" |
|
221 # |
|
222 # Code for use outside IDLE: |
|
223 if self.get_saved(): |
|
224 reply = self.maybesave() |
|
225 if reply == "cancel": |
|
226 self.text.focus_set() |
|
227 return "break" |
|
228 if not editFile: |
|
229 filename = self.askopenfile() |
|
230 else: |
|
231 filename=editFile |
|
232 if filename: |
|
233 self.loadfile(filename) |
|
234 else: |
|
235 self.text.focus_set() |
|
236 return "break" |
|
237 |
|
238 eol = r"(\r\n)|\n|\r" # \r\n (Windows), \n (UNIX), or \r (Mac) |
|
239 eol_re = re.compile(eol) |
|
240 eol_convention = os.linesep # Default |
|
241 |
|
242 def loadfile(self, filename): |
|
243 try: |
|
244 # open the file in binary mode so that we can handle |
|
245 # end-of-line convention ourselves. |
|
246 f = open(filename,'rb') |
|
247 chars = f.read() |
|
248 f.close() |
|
249 except IOError, msg: |
|
250 tkMessageBox.showerror("I/O Error", str(msg), master=self.text) |
|
251 return False |
|
252 |
|
253 chars = self.decode(chars) |
|
254 # We now convert all end-of-lines to '\n's |
|
255 firsteol = self.eol_re.search(chars) |
|
256 if firsteol: |
|
257 self.eol_convention = firsteol.group(0) |
|
258 if isinstance(self.eol_convention, unicode): |
|
259 # Make sure it is an ASCII string |
|
260 self.eol_convention = self.eol_convention.encode("ascii") |
|
261 chars = self.eol_re.sub(r"\n", chars) |
|
262 |
|
263 self.text.delete("1.0", "end") |
|
264 self.set_filename(None) |
|
265 self.text.insert("1.0", chars) |
|
266 self.reset_undo() |
|
267 self.set_filename(filename) |
|
268 self.text.mark_set("insert", "1.0") |
|
269 self.text.see("insert") |
|
270 self.updaterecentfileslist(filename) |
|
271 return True |
|
272 |
|
273 def decode(self, chars): |
|
274 """Create a Unicode string |
|
275 |
|
276 If that fails, let Tcl try its best |
|
277 """ |
|
278 # Check presence of a UTF-8 signature first |
|
279 if chars.startswith(BOM_UTF8): |
|
280 try: |
|
281 chars = chars[3:].decode("utf-8") |
|
282 except UnicodeError: |
|
283 # has UTF-8 signature, but fails to decode... |
|
284 return chars |
|
285 else: |
|
286 # Indicates that this file originally had a BOM |
|
287 self.fileencoding = BOM_UTF8 |
|
288 return chars |
|
289 # Next look for coding specification |
|
290 try: |
|
291 enc = coding_spec(chars) |
|
292 except LookupError, name: |
|
293 tkMessageBox.showerror( |
|
294 title="Error loading the file", |
|
295 message="The encoding '%s' is not known to this Python "\ |
|
296 "installation. The file may not display correctly" % name, |
|
297 master = self.text) |
|
298 enc = None |
|
299 if enc: |
|
300 try: |
|
301 return unicode(chars, enc) |
|
302 except UnicodeError: |
|
303 pass |
|
304 # If it is ASCII, we need not to record anything |
|
305 try: |
|
306 return unicode(chars, 'ascii') |
|
307 except UnicodeError: |
|
308 pass |
|
309 # Finally, try the locale's encoding. This is deprecated; |
|
310 # the user should declare a non-ASCII encoding |
|
311 try: |
|
312 chars = unicode(chars, encoding) |
|
313 self.fileencoding = encoding |
|
314 except UnicodeError: |
|
315 pass |
|
316 return chars |
|
317 |
|
318 def maybesave(self): |
|
319 if self.get_saved(): |
|
320 return "yes" |
|
321 message = "Do you want to save %s before closing?" % ( |
|
322 self.filename or "this untitled document") |
|
323 m = tkMessageBox.Message( |
|
324 title="Save On Close", |
|
325 message=message, |
|
326 icon=tkMessageBox.QUESTION, |
|
327 type=tkMessageBox.YESNOCANCEL, |
|
328 master=self.text) |
|
329 reply = m.show() |
|
330 if reply == "yes": |
|
331 self.save(None) |
|
332 if not self.get_saved(): |
|
333 reply = "cancel" |
|
334 self.text.focus_set() |
|
335 return reply |
|
336 |
|
337 def save(self, event): |
|
338 if not self.filename: |
|
339 self.save_as(event) |
|
340 else: |
|
341 if self.writefile(self.filename): |
|
342 self.set_saved(1) |
|
343 try: |
|
344 self.editwin.store_file_breaks() |
|
345 except AttributeError: # may be a PyShell |
|
346 pass |
|
347 self.text.focus_set() |
|
348 return "break" |
|
349 |
|
350 def save_as(self, event): |
|
351 filename = self.asksavefile() |
|
352 if filename: |
|
353 if self.writefile(filename): |
|
354 self.set_filename(filename) |
|
355 self.set_saved(1) |
|
356 try: |
|
357 self.editwin.store_file_breaks() |
|
358 except AttributeError: |
|
359 pass |
|
360 self.text.focus_set() |
|
361 self.updaterecentfileslist(filename) |
|
362 return "break" |
|
363 |
|
364 def save_a_copy(self, event): |
|
365 filename = self.asksavefile() |
|
366 if filename: |
|
367 self.writefile(filename) |
|
368 self.text.focus_set() |
|
369 self.updaterecentfileslist(filename) |
|
370 return "break" |
|
371 |
|
372 def writefile(self, filename): |
|
373 self.fixlastline() |
|
374 chars = self.encode(self.text.get("1.0", "end-1c")) |
|
375 if self.eol_convention != "\n": |
|
376 chars = chars.replace("\n", self.eol_convention) |
|
377 try: |
|
378 f = open(filename, "wb") |
|
379 f.write(chars) |
|
380 f.flush() |
|
381 f.close() |
|
382 return True |
|
383 except IOError, msg: |
|
384 tkMessageBox.showerror("I/O Error", str(msg), |
|
385 master=self.text) |
|
386 return False |
|
387 |
|
388 def encode(self, chars): |
|
389 if isinstance(chars, types.StringType): |
|
390 # This is either plain ASCII, or Tk was returning mixed-encoding |
|
391 # text to us. Don't try to guess further. |
|
392 return chars |
|
393 # See whether there is anything non-ASCII in it. |
|
394 # If not, no need to figure out the encoding. |
|
395 try: |
|
396 return chars.encode('ascii') |
|
397 except UnicodeError: |
|
398 pass |
|
399 # If there is an encoding declared, try this first. |
|
400 try: |
|
401 enc = coding_spec(chars) |
|
402 failed = None |
|
403 except LookupError, msg: |
|
404 failed = msg |
|
405 enc = None |
|
406 if enc: |
|
407 try: |
|
408 return chars.encode(enc) |
|
409 except UnicodeError: |
|
410 failed = "Invalid encoding '%s'" % enc |
|
411 if failed: |
|
412 tkMessageBox.showerror( |
|
413 "I/O Error", |
|
414 "%s. Saving as UTF-8" % failed, |
|
415 master = self.text) |
|
416 # If there was a UTF-8 signature, use that. This should not fail |
|
417 if self.fileencoding == BOM_UTF8 or failed: |
|
418 return BOM_UTF8 + chars.encode("utf-8") |
|
419 # Try the original file encoding next, if any |
|
420 if self.fileencoding: |
|
421 try: |
|
422 return chars.encode(self.fileencoding) |
|
423 except UnicodeError: |
|
424 tkMessageBox.showerror( |
|
425 "I/O Error", |
|
426 "Cannot save this as '%s' anymore. Saving as UTF-8" \ |
|
427 % self.fileencoding, |
|
428 master = self.text) |
|
429 return BOM_UTF8 + chars.encode("utf-8") |
|
430 # Nothing was declared, and we had not determined an encoding |
|
431 # on loading. Recommend an encoding line. |
|
432 config_encoding = idleConf.GetOption("main","EditorWindow", |
|
433 "encoding") |
|
434 if config_encoding == 'utf-8': |
|
435 # User has requested that we save files as UTF-8 |
|
436 return BOM_UTF8 + chars.encode("utf-8") |
|
437 ask_user = True |
|
438 try: |
|
439 chars = chars.encode(encoding) |
|
440 enc = encoding |
|
441 if config_encoding == 'locale': |
|
442 ask_user = False |
|
443 except UnicodeError: |
|
444 chars = BOM_UTF8 + chars.encode("utf-8") |
|
445 enc = "utf-8" |
|
446 if not ask_user: |
|
447 return chars |
|
448 dialog = EncodingMessage(self.editwin.top, enc) |
|
449 dialog.go() |
|
450 if dialog.num == 1: |
|
451 # User asked us to edit the file |
|
452 encline = "# -*- coding: %s -*-\n" % enc |
|
453 firstline = self.text.get("1.0", "2.0") |
|
454 if firstline.startswith("#!"): |
|
455 # Insert encoding after #! line |
|
456 self.text.insert("2.0", encline) |
|
457 else: |
|
458 self.text.insert("1.0", encline) |
|
459 return self.encode(self.text.get("1.0", "end-1c")) |
|
460 return chars |
|
461 |
|
462 def fixlastline(self): |
|
463 c = self.text.get("end-2c") |
|
464 if c != '\n': |
|
465 self.text.insert("end-1c", "\n") |
|
466 |
|
467 def print_window(self, event): |
|
468 m = tkMessageBox.Message( |
|
469 title="Print", |
|
470 message="Print to Default Printer", |
|
471 icon=tkMessageBox.QUESTION, |
|
472 type=tkMessageBox.OKCANCEL, |
|
473 default=tkMessageBox.OK, |
|
474 master=self.text) |
|
475 reply = m.show() |
|
476 if reply != tkMessageBox.OK: |
|
477 self.text.focus_set() |
|
478 return "break" |
|
479 tempfilename = None |
|
480 saved = self.get_saved() |
|
481 if saved: |
|
482 filename = self.filename |
|
483 # shell undo is reset after every prompt, looks saved, probably isn't |
|
484 if not saved or filename is None: |
|
485 (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_') |
|
486 filename = tempfilename |
|
487 os.close(tfd) |
|
488 if not self.writefile(tempfilename): |
|
489 os.unlink(tempfilename) |
|
490 return "break" |
|
491 platform=os.name |
|
492 printPlatform=1 |
|
493 if platform == 'posix': #posix platform |
|
494 command = idleConf.GetOption('main','General', |
|
495 'print-command-posix') |
|
496 command = command + " 2>&1" |
|
497 elif platform == 'nt': #win32 platform |
|
498 command = idleConf.GetOption('main','General','print-command-win') |
|
499 else: #no printing for this platform |
|
500 printPlatform=0 |
|
501 if printPlatform: #we can try to print for this platform |
|
502 command = command % filename |
|
503 pipe = os.popen(command, "r") |
|
504 # things can get ugly on NT if there is no printer available. |
|
505 output = pipe.read().strip() |
|
506 status = pipe.close() |
|
507 if status: |
|
508 output = "Printing failed (exit status 0x%x)\n" % \ |
|
509 status + output |
|
510 if output: |
|
511 output = "Printing command: %s\n" % repr(command) + output |
|
512 tkMessageBox.showerror("Print status", output, master=self.text) |
|
513 else: #no printing for this platform |
|
514 message="Printing is not enabled for this platform: %s" % platform |
|
515 tkMessageBox.showinfo("Print status", message, master=self.text) |
|
516 if tempfilename: |
|
517 os.unlink(tempfilename) |
|
518 return "break" |
|
519 |
|
520 opendialog = None |
|
521 savedialog = None |
|
522 |
|
523 filetypes = [ |
|
524 ("Python and text files", "*.py *.pyw *.txt", "TEXT"), |
|
525 ("All text files", "*", "TEXT"), |
|
526 ("All files", "*"), |
|
527 ] |
|
528 |
|
529 def askopenfile(self): |
|
530 dir, base = self.defaultfilename("open") |
|
531 if not self.opendialog: |
|
532 self.opendialog = tkFileDialog.Open(master=self.text, |
|
533 filetypes=self.filetypes) |
|
534 filename = self.opendialog.show(initialdir=dir, initialfile=base) |
|
535 if isinstance(filename, unicode): |
|
536 filename = filename.encode(filesystemencoding) |
|
537 return filename |
|
538 |
|
539 def defaultfilename(self, mode="open"): |
|
540 if self.filename: |
|
541 return os.path.split(self.filename) |
|
542 elif self.dirname: |
|
543 return self.dirname, "" |
|
544 else: |
|
545 try: |
|
546 pwd = os.getcwd() |
|
547 except os.error: |
|
548 pwd = "" |
|
549 return pwd, "" |
|
550 |
|
551 def asksavefile(self): |
|
552 dir, base = self.defaultfilename("save") |
|
553 if not self.savedialog: |
|
554 self.savedialog = tkFileDialog.SaveAs(master=self.text, |
|
555 filetypes=self.filetypes) |
|
556 filename = self.savedialog.show(initialdir=dir, initialfile=base) |
|
557 if isinstance(filename, unicode): |
|
558 filename = filename.encode(filesystemencoding) |
|
559 return filename |
|
560 |
|
561 def updaterecentfileslist(self,filename): |
|
562 "Update recent file list on all editor windows" |
|
563 self.editwin.update_recent_files_list(filename) |
|
564 |
|
565 def test(): |
|
566 root = Tk() |
|
567 class MyEditWin: |
|
568 def __init__(self, text): |
|
569 self.text = text |
|
570 self.flist = None |
|
571 self.text.bind("<Control-o>", self.open) |
|
572 self.text.bind("<Control-s>", self.save) |
|
573 self.text.bind("<Alt-s>", self.save_as) |
|
574 self.text.bind("<Alt-z>", self.save_a_copy) |
|
575 def get_saved(self): return 0 |
|
576 def set_saved(self, flag): pass |
|
577 def reset_undo(self): pass |
|
578 def open(self, event): |
|
579 self.text.event_generate("<<open-window-from-file>>") |
|
580 def save(self, event): |
|
581 self.text.event_generate("<<save-window>>") |
|
582 def save_as(self, event): |
|
583 self.text.event_generate("<<save-window-as-file>>") |
|
584 def save_a_copy(self, event): |
|
585 self.text.event_generate("<<save-copy-of-window-as-file>>") |
|
586 text = Text(root) |
|
587 text.pack() |
|
588 text.focus_set() |
|
589 editwin = MyEditWin(text) |
|
590 io = IOBinding(editwin) |
|
591 root.mainloop() |
|
592 |
|
593 if __name__ == "__main__": |
|
594 test() |