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