iomenu.py 16 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. from idlelib.config import idleConf
  11. from idlelib.util import py_extensions
  12. py_extensions = ' '.join("*"+ext for ext in py_extensions)
  13. encoding = 'utf-8'
  14. errors = 'surrogatepass' if sys.platform == 'win32' else 'surrogateescape'
  15. class IOBinding:
  16. # One instance per editor Window so methods know which to save, close.
  17. # Open returns focus to self.editwin if aborted.
  18. # EditorWindow.open_module, others, belong here.
  19. def __init__(self, editwin):
  20. self.editwin = editwin
  21. self.text = editwin.text
  22. self.__id_open = self.text.bind("<<open-window-from-file>>", self.open)
  23. self.__id_save = self.text.bind("<<save-window>>", self.save)
  24. self.__id_saveas = self.text.bind("<<save-window-as-file>>",
  25. self.save_as)
  26. self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
  27. self.save_a_copy)
  28. self.fileencoding = 'utf-8'
  29. self.__id_print = self.text.bind("<<print-window>>", self.print_window)
  30. def close(self):
  31. # Undo command bindings
  32. self.text.unbind("<<open-window-from-file>>", self.__id_open)
  33. self.text.unbind("<<save-window>>", self.__id_save)
  34. self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
  35. self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
  36. self.text.unbind("<<print-window>>", self.__id_print)
  37. # Break cycles
  38. self.editwin = None
  39. self.text = None
  40. self.filename_change_hook = None
  41. def get_saved(self):
  42. return self.editwin.get_saved()
  43. def set_saved(self, flag):
  44. self.editwin.set_saved(flag)
  45. def reset_undo(self):
  46. self.editwin.reset_undo()
  47. filename_change_hook = None
  48. def set_filename_change_hook(self, hook):
  49. self.filename_change_hook = hook
  50. filename = None
  51. dirname = None
  52. def set_filename(self, filename):
  53. if filename and os.path.isdir(filename):
  54. self.filename = None
  55. self.dirname = filename
  56. else:
  57. self.filename = filename
  58. self.dirname = None
  59. self.set_saved(1)
  60. if self.filename_change_hook:
  61. self.filename_change_hook()
  62. def open(self, event=None, editFile=None):
  63. flist = self.editwin.flist
  64. # Save in case parent window is closed (ie, during askopenfile()).
  65. if flist:
  66. if not editFile:
  67. filename = self.askopenfile()
  68. else:
  69. filename=editFile
  70. if filename:
  71. # If editFile is valid and already open, flist.open will
  72. # shift focus to its existing window.
  73. # If the current window exists and is a fresh unnamed,
  74. # unmodified editor window (not an interpreter shell),
  75. # pass self.loadfile to flist.open so it will load the file
  76. # in the current window (if the file is not already open)
  77. # instead of a new window.
  78. if (self.editwin and
  79. not getattr(self.editwin, 'interp', None) and
  80. not self.filename and
  81. self.get_saved()):
  82. flist.open(filename, self.loadfile)
  83. else:
  84. flist.open(filename)
  85. else:
  86. if self.text:
  87. self.text.focus_set()
  88. return "break"
  89. # Code for use outside IDLE:
  90. if self.get_saved():
  91. reply = self.maybesave()
  92. if reply == "cancel":
  93. self.text.focus_set()
  94. return "break"
  95. if not editFile:
  96. filename = self.askopenfile()
  97. else:
  98. filename=editFile
  99. if filename:
  100. self.loadfile(filename)
  101. else:
  102. self.text.focus_set()
  103. return "break"
  104. eol_convention = os.linesep # default
  105. def loadfile(self, filename):
  106. try:
  107. try:
  108. with tokenize.open(filename) as f:
  109. chars = f.read()
  110. fileencoding = f.encoding
  111. eol_convention = f.newlines
  112. converted = False
  113. except (UnicodeDecodeError, SyntaxError):
  114. # Wait for the editor window to appear
  115. self.editwin.text.update()
  116. enc = askstring(
  117. "Specify file encoding",
  118. "The file's encoding is invalid for Python 3.x.\n"
  119. "IDLE will convert it to UTF-8.\n"
  120. "What is the current encoding of the file?",
  121. initialvalue='utf-8',
  122. parent=self.editwin.text)
  123. with open(filename, encoding=enc) as f:
  124. chars = f.read()
  125. fileencoding = f.encoding
  126. eol_convention = f.newlines
  127. converted = True
  128. except OSError as err:
  129. messagebox.showerror("I/O Error", str(err), parent=self.text)
  130. return False
  131. except UnicodeDecodeError:
  132. messagebox.showerror("Decoding Error",
  133. "File %s\nFailed to Decode" % filename,
  134. parent=self.text)
  135. return False
  136. if not isinstance(eol_convention, str):
  137. # If the file does not contain line separators, it is None.
  138. # If the file contains mixed line separators, it is a tuple.
  139. if eol_convention is not None:
  140. messagebox.showwarning("Mixed Newlines",
  141. "Mixed newlines detected.\n"
  142. "The file will be changed on save.",
  143. parent=self.text)
  144. converted = True
  145. eol_convention = os.linesep # default
  146. self.text.delete("1.0", "end")
  147. self.set_filename(None)
  148. self.fileencoding = fileencoding
  149. self.eol_convention = eol_convention
  150. self.text.insert("1.0", chars)
  151. self.reset_undo()
  152. self.set_filename(filename)
  153. if converted:
  154. # We need to save the conversion results first
  155. # before being able to execute the code
  156. self.set_saved(False)
  157. self.text.mark_set("insert", "1.0")
  158. self.text.yview("insert")
  159. self.updaterecentfileslist(filename)
  160. return True
  161. def maybesave(self):
  162. if self.get_saved():
  163. return "yes"
  164. message = "Do you want to save %s before closing?" % (
  165. self.filename or "this untitled document")
  166. confirm = messagebox.askyesnocancel(
  167. title="Save On Close",
  168. message=message,
  169. default=messagebox.YES,
  170. parent=self.text)
  171. if confirm:
  172. reply = "yes"
  173. self.save(None)
  174. if not self.get_saved():
  175. reply = "cancel"
  176. elif confirm is None:
  177. reply = "cancel"
  178. else:
  179. reply = "no"
  180. self.text.focus_set()
  181. return reply
  182. def save(self, event):
  183. if not self.filename:
  184. self.save_as(event)
  185. else:
  186. if self.writefile(self.filename):
  187. self.set_saved(True)
  188. try:
  189. self.editwin.store_file_breaks()
  190. except AttributeError: # may be a PyShell
  191. pass
  192. self.text.focus_set()
  193. return "break"
  194. def save_as(self, event):
  195. filename = self.asksavefile()
  196. if filename:
  197. if self.writefile(filename):
  198. self.set_filename(filename)
  199. self.set_saved(1)
  200. try:
  201. self.editwin.store_file_breaks()
  202. except AttributeError:
  203. pass
  204. self.text.focus_set()
  205. self.updaterecentfileslist(filename)
  206. return "break"
  207. def save_a_copy(self, event):
  208. filename = self.asksavefile()
  209. if filename:
  210. self.writefile(filename)
  211. self.text.focus_set()
  212. self.updaterecentfileslist(filename)
  213. return "break"
  214. def writefile(self, filename):
  215. text = self.fixnewlines()
  216. chars = self.encode(text)
  217. try:
  218. with open(filename, "wb") as f:
  219. f.write(chars)
  220. f.flush()
  221. os.fsync(f.fileno())
  222. return True
  223. except OSError as msg:
  224. messagebox.showerror("I/O Error", str(msg),
  225. parent=self.text)
  226. return False
  227. def fixnewlines(self):
  228. """Return text with os eols.
  229. Add prompts if shell else final \n if missing.
  230. """
  231. if hasattr(self.editwin, "interp"): # Saving shell.
  232. text = self.editwin.get_prompt_text('1.0', self.text.index('end-1c'))
  233. else:
  234. if self.text.get("end-2c") != '\n':
  235. self.text.insert("end-1c", "\n") # Changes 'end-1c' value.
  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)