IOBinding.py 21 KB

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