iomenu.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import io
  2. import os
  3. import shlex
  4. import sys
  5. import tempfile
  6. import tokenize
  7. from tkinter import filedialog
  8. from tkinter import messagebox
  9. from tkinter.simpledialog import askstring
  10. import idlelib
  11. from idlelib.config import idleConf
  12. from idlelib.util import py_extensions
  13. py_extensions = ' '.join("*"+ext for ext in py_extensions)
  14. encoding = 'utf-8'
  15. if sys.platform == 'win32':
  16. errors = 'surrogatepass'
  17. else:
  18. errors = 'surrogateescape'
  19. class IOBinding:
  20. # One instance per editor Window so methods know which to save, close.
  21. # Open returns focus to self.editwin if aborted.
  22. # EditorWindow.open_module, others, belong here.
  23. def __init__(self, editwin):
  24. self.editwin = editwin
  25. self.text = editwin.text
  26. self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
  27. self.__id_save = self.text.bind("<<save-window>>", self.save)
  28. self.__id_saveas = self.text.bind("<<save-window-as-file>>",
  29. self.save_as)
  30. self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
  31. self.save_a_copy)
  32. self.fileencoding = 'utf-8'
  33. self.__id_print = self.text.bind("<<print-window>>", self.print_window)
  34. def close(self):
  35. # Undo command bindings
  36. self.text.unbind("<<open-window-from-file>>", self.__id_open)
  37. self.text.unbind("<<save-window>>", self.__id_save)
  38. self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
  39. self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
  40. self.text.unbind("<<print-window>>", self.__id_print)
  41. # Break cycles
  42. self.editwin = None
  43. self.text = None
  44. self.filename_change_hook = None
  45. def get_saved(self):
  46. return self.editwin.get_saved()
  47. def set_saved(self, flag):
  48. self.editwin.set_saved(flag)
  49. def reset_undo(self):
  50. self.editwin.reset_undo()
  51. filename_change_hook = None
  52. def set_filename_change_hook(self, hook):
  53. self.filename_change_hook = hook
  54. filename = None
  55. dirname = None
  56. def set_filename(self, filename):
  57. if filename and os.path.isdir(filename):
  58. self.filename = None
  59. self.dirname = filename
  60. else:
  61. self.filename = filename
  62. self.dirname = None
  63. self.set_saved(1)
  64. if self.filename_change_hook:
  65. self.filename_change_hook()
  66. def open(self, event=None, editFile=None):
  67. flist = self.editwin.flist
  68. # Save in case parent window is closed (ie, during askopenfile()).
  69. if flist:
  70. if not editFile:
  71. filename = self.askopenfile()
  72. else:
  73. filename=editFile
  74. if filename:
  75. # If editFile is valid and already open, flist.open will
  76. # shift focus to its existing window.
  77. # If the current window exists and is a fresh unnamed,
  78. # unmodified editor window (not an interpreter shell),
  79. # pass self.loadfile to flist.open so it will load the file
  80. # in the current window (if the file is not already open)
  81. # instead of a new window.
  82. if (self.editwin and
  83. not getattr(self.editwin, 'interp', None) and
  84. not self.filename and
  85. self.get_saved()):
  86. flist.open(filename, self.loadfile)
  87. else:
  88. flist.open(filename)
  89. else:
  90. if self.text:
  91. self.text.focus_set()
  92. return "break"
  93. # Code for use outside IDLE:
  94. if self.get_saved():
  95. reply = self.maybesave()
  96. if reply == "cancel":
  97. self.text.focus_set()
  98. return "break"
  99. if not editFile:
  100. filename = self.askopenfile()
  101. else:
  102. filename=editFile
  103. if filename:
  104. self.loadfile(filename)
  105. else:
  106. self.text.focus_set()
  107. return "break"
  108. eol_convention = os.linesep # default
  109. def loadfile(self, filename):
  110. try:
  111. try:
  112. with tokenize.open(filename) as f:
  113. chars = f.read()
  114. fileencoding = f.encoding
  115. eol_convention = f.newlines
  116. converted = False
  117. except (UnicodeDecodeError, SyntaxError):
  118. # Wait for the editor window to appear
  119. self.editwin.text.update()
  120. enc = askstring(
  121. "Specify file encoding",
  122. "The file's encoding is invalid for Python 3.x.\n"
  123. "IDLE will convert it to UTF-8.\n"
  124. "What is the current encoding of the file?",
  125. initialvalue='utf-8',
  126. parent=self.editwin.text)
  127. with open(filename, encoding=enc) as f:
  128. chars = f.read()
  129. fileencoding = f.encoding
  130. eol_convention = f.newlines
  131. converted = True
  132. except OSError as err:
  133. messagebox.showerror("I/O Error", str(err), parent=self.text)
  134. return False
  135. except UnicodeDecodeError:
  136. messagebox.showerror("Decoding Error",
  137. "File %s\nFailed to Decode" % filename,
  138. parent=self.text)
  139. return False
  140. if not isinstance(eol_convention, str):
  141. # If the file does not contain line separators, it is None.
  142. # If the file contains mixed line separators, it is a tuple.
  143. if eol_convention is not None:
  144. messagebox.showwarning("Mixed Newlines",
  145. "Mixed newlines detected.\n"
  146. "The file will be changed on save.",
  147. parent=self.text)
  148. converted = True
  149. eol_convention = os.linesep # default
  150. self.text.delete("1.0", "end")
  151. self.set_filename(None)
  152. self.fileencoding = fileencoding
  153. self.eol_convention = eol_convention
  154. self.text.insert("1.0", chars)
  155. self.reset_undo()
  156. self.set_filename(filename)
  157. if converted:
  158. # We need to save the conversion results first
  159. # before being able to execute the code
  160. self.set_saved(False)
  161. self.text.mark_set("insert", "1.0")
  162. self.text.yview("insert")
  163. self.updaterecentfileslist(filename)
  164. return True
  165. def maybesave(self):
  166. if self.get_saved():
  167. return "yes"
  168. message = "Do you want to save %s before closing?" % (
  169. self.filename or "this untitled document")
  170. confirm = messagebox.askyesnocancel(
  171. title="Save On Close",
  172. message=message,
  173. default=messagebox.YES,
  174. parent=self.text)
  175. if confirm:
  176. reply = "yes"
  177. self.save(None)
  178. if not self.get_saved():
  179. reply = "cancel"
  180. elif confirm is None:
  181. reply = "cancel"
  182. else:
  183. reply = "no"
  184. self.text.focus_set()
  185. return reply
  186. def save(self, event):
  187. if not self.filename:
  188. self.save_as(event)
  189. else:
  190. if self.writefile(self.filename):
  191. self.set_saved(True)
  192. try:
  193. self.editwin.store_file_breaks()
  194. except AttributeError: # may be a PyShell
  195. pass
  196. self.text.focus_set()
  197. return "break"
  198. def save_as(self, event):
  199. filename = self.asksavefile()
  200. if filename:
  201. if self.writefile(filename):
  202. self.set_filename(filename)
  203. self.set_saved(1)
  204. try:
  205. self.editwin.store_file_breaks()
  206. except AttributeError:
  207. pass
  208. self.text.focus_set()
  209. self.updaterecentfileslist(filename)
  210. return "break"
  211. def save_a_copy(self, event):
  212. filename = self.asksavefile()
  213. if filename:
  214. self.writefile(filename)
  215. self.text.focus_set()
  216. self.updaterecentfileslist(filename)
  217. return "break"
  218. def writefile(self, filename):
  219. text = self.fixnewlines()
  220. chars = self.encode(text)
  221. try:
  222. with open(filename, "wb") as f:
  223. f.write(chars)
  224. f.flush()
  225. os.fsync(f.fileno())
  226. return True
  227. except OSError as msg:
  228. messagebox.showerror("I/O Error", str(msg),
  229. parent=self.text)
  230. return False
  231. def fixnewlines(self):
  232. "Return text with final \n if needed and os eols."
  233. if (self.text.get("end-2c") != '\n'
  234. and not hasattr(self.editwin, "interp")): # Not shell.
  235. self.text.insert("end-1c", "\n")
  236. text = self.text.get("1.0", "end-1c")
  237. if self.eol_convention != "\n":
  238. text = text.replace("\n", self.eol_convention)
  239. return text
  240. def encode(self, chars):
  241. if isinstance(chars, bytes):
  242. # This is either plain ASCII, or Tk was returning mixed-encoding
  243. # text to us. Don't try to guess further.
  244. return chars
  245. # Preserve a BOM that might have been present on opening
  246. if self.fileencoding == 'utf-8-sig':
  247. return chars.encode('utf-8-sig')
  248. # See whether there is anything non-ASCII in it.
  249. # If not, no need to figure out the encoding.
  250. try:
  251. return chars.encode('ascii')
  252. except UnicodeEncodeError:
  253. pass
  254. # Check if there is an encoding declared
  255. try:
  256. encoded = chars.encode('ascii', 'replace')
  257. enc, _ = tokenize.detect_encoding(io.BytesIO(encoded).readline)
  258. return chars.encode(enc)
  259. except SyntaxError as err:
  260. failed = str(err)
  261. except UnicodeEncodeError:
  262. failed = "Invalid encoding '%s'" % enc
  263. messagebox.showerror(
  264. "I/O Error",
  265. "%s.\nSaving as UTF-8" % failed,
  266. parent=self.text)
  267. # Fallback: save as UTF-8, with BOM - ignoring the incorrect
  268. # declared encoding
  269. return chars.encode('utf-8-sig')
  270. def print_window(self, event):
  271. confirm = messagebox.askokcancel(
  272. title="Print",
  273. message="Print to Default Printer",
  274. default=messagebox.OK,
  275. parent=self.text)
  276. if not confirm:
  277. self.text.focus_set()
  278. return "break"
  279. tempfilename = None
  280. saved = self.get_saved()
  281. if saved:
  282. filename = self.filename
  283. # shell undo is reset after every prompt, looks saved, probably isn't
  284. if not saved or filename is None:
  285. (tfd, tempfilename) = tempfile.mkstemp(prefix='IDLE_tmp_')
  286. filename = tempfilename
  287. os.close(tfd)
  288. if not self.writefile(tempfilename):
  289. os.unlink(tempfilename)
  290. return "break"
  291. platform = os.name
  292. printPlatform = True
  293. if platform == 'posix': #posix platform
  294. command = idleConf.GetOption('main','General',
  295. 'print-command-posix')
  296. command = command + " 2>&1"
  297. elif platform == 'nt': #win32 platform
  298. command = idleConf.GetOption('main','General','print-command-win')
  299. else: #no printing for this platform
  300. printPlatform = False
  301. if printPlatform: #we can try to print for this platform
  302. command = command % shlex.quote(filename)
  303. pipe = os.popen(command, "r")
  304. # things can get ugly on NT if there is no printer available.
  305. output = pipe.read().strip()
  306. status = pipe.close()
  307. if status:
  308. output = "Printing failed (exit status 0x%x)\n" % \
  309. status + output
  310. if output:
  311. output = "Printing command: %s\n" % repr(command) + output
  312. messagebox.showerror("Print status", output, parent=self.text)
  313. else: #no printing for this platform
  314. message = "Printing is not enabled for this platform: %s" % platform
  315. messagebox.showinfo("Print status", message, parent=self.text)
  316. if tempfilename:
  317. os.unlink(tempfilename)
  318. return "break"
  319. opendialog = None
  320. savedialog = None
  321. filetypes = (
  322. ("Python files", py_extensions, "TEXT"),
  323. ("Text files", "*.txt", "TEXT"),
  324. ("All files", "*"),
  325. )
  326. defaultextension = '.py' if sys.platform == 'darwin' else ''
  327. def askopenfile(self):
  328. dir, base = self.defaultfilename("open")
  329. if not self.opendialog:
  330. self.opendialog = filedialog.Open(parent=self.text,
  331. filetypes=self.filetypes)
  332. filename = self.opendialog.show(initialdir=dir, initialfile=base)
  333. return filename
  334. def defaultfilename(self, mode="open"):
  335. if self.filename:
  336. return os.path.split(self.filename)
  337. elif self.dirname:
  338. return self.dirname, ""
  339. else:
  340. try:
  341. pwd = os.getcwd()
  342. except OSError:
  343. pwd = ""
  344. return pwd, ""
  345. def asksavefile(self):
  346. dir, base = self.defaultfilename("save")
  347. if not self.savedialog:
  348. self.savedialog = filedialog.SaveAs(
  349. parent=self.text,
  350. filetypes=self.filetypes,
  351. defaultextension=self.defaultextension)
  352. filename = self.savedialog.show(initialdir=dir, initialfile=base)
  353. return filename
  354. def updaterecentfileslist(self,filename):
  355. "Update recent file list on all editor windows"
  356. if self.editwin.flist:
  357. self.editwin.update_recent_files_list(filename)
  358. def _io_binding(parent): # htest #
  359. from tkinter import Toplevel, Text
  360. root = Toplevel(parent)
  361. root.title("Test IOBinding")
  362. x, y = map(int, parent.geometry().split('+')[1:])
  363. root.geometry("+%d+%d" % (x, y + 175))
  364. class MyEditWin:
  365. def __init__(self, text):
  366. self.text = text
  367. self.flist = None
  368. self.text.bind("<Control-o>", self.open)
  369. self.text.bind('<Control-p>', self.print)
  370. self.text.bind("<Control-s>", self.save)
  371. self.text.bind("<Alt-s>", self.saveas)
  372. self.text.bind('<Control-c>', self.savecopy)
  373. def get_saved(self): return 0
  374. def set_saved(self, flag): pass
  375. def reset_undo(self): pass
  376. def open(self, event):
  377. self.text.event_generate("<<open-window-from-file>>")
  378. def print(self, event):
  379. self.text.event_generate("<<print-window>>")
  380. def save(self, event):
  381. self.text.event_generate("<<save-window>>")
  382. def saveas(self, event):
  383. self.text.event_generate("<<save-window-as-file>>")
  384. def savecopy(self, event):
  385. self.text.event_generate("<<save-copy-of-window-as-file>>")
  386. text = Text(root)
  387. text.pack()
  388. text.focus_set()
  389. editwin = MyEditWin(text)
  390. IOBinding(editwin)
  391. if __name__ == "__main__":
  392. from unittest import main
  393. main('idlelib.idle_test.test_iomenu', verbosity=2, exit=False)
  394. from idlelib.idle_test.htest import run
  395. run(_io_binding)