PageRenderTime 71ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/Lib/idlelib/EditorWindow.py

http://unladen-swallow.googlecode.com/
Python | 1567 lines | 1300 code | 129 blank | 138 comment | 195 complexity | 7c01a03797facd6f0d3f9b66f279a62f MD5 | raw file
Possible License(s): 0BSD, BSD-3-Clause
  1. import sys
  2. import os
  3. import re
  4. import imp
  5. from itertools import count
  6. from Tkinter import *
  7. import tkSimpleDialog
  8. import tkMessageBox
  9. from MultiCall import MultiCallCreator
  10. import webbrowser
  11. import idlever
  12. import WindowList
  13. import SearchDialog
  14. import GrepDialog
  15. import ReplaceDialog
  16. import PyParse
  17. from configHandler import idleConf
  18. import aboutDialog, textView, configDialog
  19. import macosxSupport
  20. # The default tab setting for a Text widget, in average-width characters.
  21. TK_TABWIDTH_DEFAULT = 8
  22. def _sphinx_version():
  23. "Format sys.version_info to produce the Sphinx version string used to install the chm docs"
  24. major, minor, micro, level, serial = sys.version_info
  25. release = '%s%s' % (major, minor)
  26. if micro:
  27. release += '%s' % micro
  28. if level != 'final':
  29. release += '%s%s' % (level[0], serial)
  30. return release
  31. def _find_module(fullname, path=None):
  32. """Version of imp.find_module() that handles hierarchical module names"""
  33. file = None
  34. for tgt in fullname.split('.'):
  35. if file is not None:
  36. file.close() # close intermediate files
  37. (file, filename, descr) = imp.find_module(tgt, path)
  38. if descr[2] == imp.PY_SOURCE:
  39. break # find but not load the source file
  40. module = imp.load_module(tgt, file, filename, descr)
  41. try:
  42. path = module.__path__
  43. except AttributeError:
  44. raise ImportError, 'No source for module ' + module.__name__
  45. return file, filename, descr
  46. class EditorWindow(object):
  47. from Percolator import Percolator
  48. from ColorDelegator import ColorDelegator
  49. from UndoDelegator import UndoDelegator
  50. from IOBinding import IOBinding, filesystemencoding, encoding
  51. import Bindings
  52. from Tkinter import Toplevel
  53. from MultiStatusBar import MultiStatusBar
  54. help_url = None
  55. def __init__(self, flist=None, filename=None, key=None, root=None):
  56. if EditorWindow.help_url is None:
  57. dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
  58. if sys.platform.count('linux'):
  59. # look for html docs in a couple of standard places
  60. pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
  61. if os.path.isdir('/var/www/html/python/'): # "python2" rpm
  62. dochome = '/var/www/html/python/index.html'
  63. else:
  64. basepath = '/usr/share/doc/' # standard location
  65. dochome = os.path.join(basepath, pyver,
  66. 'Doc', 'index.html')
  67. elif sys.platform[:3] == 'win':
  68. chmfile = os.path.join(sys.prefix, 'Doc',
  69. 'Python%s.chm' % _sphinx_version())
  70. if os.path.isfile(chmfile):
  71. dochome = chmfile
  72. elif macosxSupport.runningAsOSXApp():
  73. # documentation is stored inside the python framework
  74. dochome = os.path.join(sys.prefix,
  75. 'Resources/English.lproj/Documentation/index.html')
  76. dochome = os.path.normpath(dochome)
  77. if os.path.isfile(dochome):
  78. EditorWindow.help_url = dochome
  79. if sys.platform == 'darwin':
  80. # Safari requires real file:-URLs
  81. EditorWindow.help_url = 'file://' + EditorWindow.help_url
  82. else:
  83. EditorWindow.help_url = "http://docs.python.org/%d.%d" % sys.version_info[:2]
  84. currentTheme=idleConf.CurrentTheme()
  85. self.flist = flist
  86. root = root or flist.root
  87. self.root = root
  88. try:
  89. sys.ps1
  90. except AttributeError:
  91. sys.ps1 = '>>> '
  92. self.menubar = Menu(root)
  93. self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
  94. if flist:
  95. self.tkinter_vars = flist.vars
  96. #self.top.instance_dict makes flist.inversedict avalable to
  97. #configDialog.py so it can access all EditorWindow instaces
  98. self.top.instance_dict = flist.inversedict
  99. else:
  100. self.tkinter_vars = {} # keys: Tkinter event names
  101. # values: Tkinter variable instances
  102. self.top.instance_dict = {}
  103. self.recent_files_path = os.path.join(idleConf.GetUserCfgDir(),
  104. 'recent-files.lst')
  105. self.text_frame = text_frame = Frame(top)
  106. self.vbar = vbar = Scrollbar(text_frame, name='vbar')
  107. self.width = idleConf.GetOption('main','EditorWindow','width')
  108. text_options = {
  109. 'name': 'text',
  110. 'padx': 5,
  111. 'wrap': 'none',
  112. 'width': self.width,
  113. 'height': idleConf.GetOption('main', 'EditorWindow', 'height')}
  114. if TkVersion >= 8.5:
  115. # Starting with tk 8.5 we have to set the new tabstyle option
  116. # to 'wordprocessor' to achieve the same display of tabs as in
  117. # older tk versions.
  118. text_options['tabstyle'] = 'wordprocessor'
  119. self.text = text = MultiCallCreator(Text)(text_frame, **text_options)
  120. self.top.focused_widget = self.text
  121. self.createmenubar()
  122. self.apply_bindings()
  123. self.top.protocol("WM_DELETE_WINDOW", self.close)
  124. self.top.bind("<<close-window>>", self.close_event)
  125. if macosxSupport.runningAsOSXApp():
  126. # Command-W on editorwindows doesn't work without this.
  127. text.bind('<<close-window>>', self.close_event)
  128. text.bind("<<cut>>", self.cut)
  129. text.bind("<<copy>>", self.copy)
  130. text.bind("<<paste>>", self.paste)
  131. text.bind("<<center-insert>>", self.center_insert_event)
  132. text.bind("<<help>>", self.help_dialog)
  133. text.bind("<<python-docs>>", self.python_docs)
  134. text.bind("<<about-idle>>", self.about_dialog)
  135. text.bind("<<open-config-dialog>>", self.config_dialog)
  136. text.bind("<<open-module>>", self.open_module)
  137. text.bind("<<do-nothing>>", lambda event: "break")
  138. text.bind("<<select-all>>", self.select_all)
  139. text.bind("<<remove-selection>>", self.remove_selection)
  140. text.bind("<<find>>", self.find_event)
  141. text.bind("<<find-again>>", self.find_again_event)
  142. text.bind("<<find-in-files>>", self.find_in_files_event)
  143. text.bind("<<find-selection>>", self.find_selection_event)
  144. text.bind("<<replace>>", self.replace_event)
  145. text.bind("<<goto-line>>", self.goto_line_event)
  146. text.bind("<3>", self.right_menu_event)
  147. text.bind("<<smart-backspace>>",self.smart_backspace_event)
  148. text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
  149. text.bind("<<smart-indent>>",self.smart_indent_event)
  150. text.bind("<<indent-region>>",self.indent_region_event)
  151. text.bind("<<dedent-region>>",self.dedent_region_event)
  152. text.bind("<<comment-region>>",self.comment_region_event)
  153. text.bind("<<uncomment-region>>",self.uncomment_region_event)
  154. text.bind("<<tabify-region>>",self.tabify_region_event)
  155. text.bind("<<untabify-region>>",self.untabify_region_event)
  156. text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
  157. text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
  158. text.bind("<Left>", self.move_at_edge_if_selection(0))
  159. text.bind("<Right>", self.move_at_edge_if_selection(1))
  160. text.bind("<<del-word-left>>", self.del_word_left)
  161. text.bind("<<del-word-right>>", self.del_word_right)
  162. text.bind("<<beginning-of-line>>", self.home_callback)
  163. if flist:
  164. flist.inversedict[self] = key
  165. if key:
  166. flist.dict[key] = self
  167. text.bind("<<open-new-window>>", self.new_callback)
  168. text.bind("<<close-all-windows>>", self.flist.close_all_callback)
  169. text.bind("<<open-class-browser>>", self.open_class_browser)
  170. text.bind("<<open-path-browser>>", self.open_path_browser)
  171. self.set_status_bar()
  172. vbar['command'] = text.yview
  173. vbar.pack(side=RIGHT, fill=Y)
  174. text['yscrollcommand'] = vbar.set
  175. fontWeight = 'normal'
  176. if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'):
  177. fontWeight='bold'
  178. text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
  179. idleConf.GetOption('main', 'EditorWindow', 'font-size'),
  180. fontWeight))
  181. text_frame.pack(side=LEFT, fill=BOTH, expand=1)
  182. text.pack(side=TOP, fill=BOTH, expand=1)
  183. text.focus_set()
  184. # usetabs true -> literal tab characters are used by indent and
  185. # dedent cmds, possibly mixed with spaces if
  186. # indentwidth is not a multiple of tabwidth,
  187. # which will cause Tabnanny to nag!
  188. # false -> tab characters are converted to spaces by indent
  189. # and dedent cmds, and ditto TAB keystrokes
  190. # Although use-spaces=0 can be configured manually in config-main.def,
  191. # configuration of tabs v. spaces is not supported in the configuration
  192. # dialog. IDLE promotes the preferred Python indentation: use spaces!
  193. usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool')
  194. self.usetabs = not usespaces
  195. # tabwidth is the display width of a literal tab character.
  196. # CAUTION: telling Tk to use anything other than its default
  197. # tab setting causes it to use an entirely different tabbing algorithm,
  198. # treating tab stops as fixed distances from the left margin.
  199. # Nobody expects this, so for now tabwidth should never be changed.
  200. self.tabwidth = 8 # must remain 8 until Tk is fixed.
  201. # indentwidth is the number of screen characters per indent level.
  202. # The recommended Python indentation is four spaces.
  203. self.indentwidth = self.tabwidth
  204. self.set_notabs_indentwidth()
  205. # If context_use_ps1 is true, parsing searches back for a ps1 line;
  206. # else searches for a popular (if, def, ...) Python stmt.
  207. self.context_use_ps1 = False
  208. # When searching backwards for a reliable place to begin parsing,
  209. # first start num_context_lines[0] lines back, then
  210. # num_context_lines[1] lines back if that didn't work, and so on.
  211. # The last value should be huge (larger than the # of lines in a
  212. # conceivable file).
  213. # Making the initial values larger slows things down more often.
  214. self.num_context_lines = 50, 500, 5000000
  215. self.per = per = self.Percolator(text)
  216. self.undo = undo = self.UndoDelegator()
  217. per.insertfilter(undo)
  218. text.undo_block_start = undo.undo_block_start
  219. text.undo_block_stop = undo.undo_block_stop
  220. undo.set_saved_change_hook(self.saved_change_hook)
  221. # IOBinding implements file I/O and printing functionality
  222. self.io = io = self.IOBinding(self)
  223. io.set_filename_change_hook(self.filename_change_hook)
  224. # Create the recent files submenu
  225. self.recent_files_menu = Menu(self.menubar)
  226. self.menudict['file'].insert_cascade(3, label='Recent Files',
  227. underline=0,
  228. menu=self.recent_files_menu)
  229. self.update_recent_files_list()
  230. self.color = None # initialized below in self.ResetColorizer
  231. if filename:
  232. if os.path.exists(filename) and not os.path.isdir(filename):
  233. io.loadfile(filename)
  234. else:
  235. io.set_filename(filename)
  236. self.ResetColorizer()
  237. self.saved_change_hook()
  238. self.set_indentation_params(self.ispythonsource(filename))
  239. self.load_extensions()
  240. menu = self.menudict.get('windows')
  241. if menu:
  242. end = menu.index("end")
  243. if end is None:
  244. end = -1
  245. if end >= 0:
  246. menu.add_separator()
  247. end = end + 1
  248. self.wmenu_end = end
  249. WindowList.register_callback(self.postwindowsmenu)
  250. # Some abstractions so IDLE extensions are cross-IDE
  251. self.askyesno = tkMessageBox.askyesno
  252. self.askinteger = tkSimpleDialog.askinteger
  253. self.showerror = tkMessageBox.showerror
  254. def _filename_to_unicode(self, filename):
  255. """convert filename to unicode in order to display it in Tk"""
  256. if isinstance(filename, unicode) or not filename:
  257. return filename
  258. else:
  259. try:
  260. return filename.decode(self.filesystemencoding)
  261. except UnicodeDecodeError:
  262. # XXX
  263. try:
  264. return filename.decode(self.encoding)
  265. except UnicodeDecodeError:
  266. # byte-to-byte conversion
  267. return filename.decode('iso8859-1')
  268. def new_callback(self, event):
  269. dirname, basename = self.io.defaultfilename()
  270. self.flist.new(dirname)
  271. return "break"
  272. def home_callback(self, event):
  273. if (event.state & 12) != 0 and event.keysym == "Home":
  274. # state&1==shift, state&4==control, state&8==alt
  275. return # <Modifier-Home>; fall back to class binding
  276. if self.text.index("iomark") and \
  277. self.text.compare("iomark", "<=", "insert lineend") and \
  278. self.text.compare("insert linestart", "<=", "iomark"):
  279. insertpt = int(self.text.index("iomark").split(".")[1])
  280. else:
  281. line = self.text.get("insert linestart", "insert lineend")
  282. for insertpt in xrange(len(line)):
  283. if line[insertpt] not in (' ','\t'):
  284. break
  285. else:
  286. insertpt=len(line)
  287. lineat = int(self.text.index("insert").split('.')[1])
  288. if insertpt == lineat:
  289. insertpt = 0
  290. dest = "insert linestart+"+str(insertpt)+"c"
  291. if (event.state&1) == 0:
  292. # shift not pressed
  293. self.text.tag_remove("sel", "1.0", "end")
  294. else:
  295. if not self.text.index("sel.first"):
  296. self.text.mark_set("anchor","insert")
  297. first = self.text.index(dest)
  298. last = self.text.index("anchor")
  299. if self.text.compare(first,">",last):
  300. first,last = last,first
  301. self.text.tag_remove("sel", "1.0", "end")
  302. self.text.tag_add("sel", first, last)
  303. self.text.mark_set("insert", dest)
  304. self.text.see("insert")
  305. return "break"
  306. def set_status_bar(self):
  307. self.status_bar = self.MultiStatusBar(self.top)
  308. if macosxSupport.runningAsOSXApp():
  309. # Insert some padding to avoid obscuring some of the statusbar
  310. # by the resize widget.
  311. self.status_bar.set_label('_padding1', ' ', side=RIGHT)
  312. self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
  313. self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
  314. self.status_bar.pack(side=BOTTOM, fill=X)
  315. self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
  316. self.text.event_add("<<set-line-and-column>>",
  317. "<KeyRelease>", "<ButtonRelease>")
  318. self.text.after_idle(self.set_line_and_column)
  319. def set_line_and_column(self, event=None):
  320. line, column = self.text.index(INSERT).split('.')
  321. self.status_bar.set_label('column', 'Col: %s' % column)
  322. self.status_bar.set_label('line', 'Ln: %s' % line)
  323. menu_specs = [
  324. ("file", "_File"),
  325. ("edit", "_Edit"),
  326. ("format", "F_ormat"),
  327. ("run", "_Run"),
  328. ("options", "_Options"),
  329. ("windows", "_Windows"),
  330. ("help", "_Help"),
  331. ]
  332. if macosxSupport.runningAsOSXApp():
  333. del menu_specs[-3]
  334. menu_specs[-2] = ("windows", "_Window")
  335. def createmenubar(self):
  336. mbar = self.menubar
  337. self.menudict = menudict = {}
  338. for name, label in self.menu_specs:
  339. underline, label = prepstr(label)
  340. menudict[name] = menu = Menu(mbar, name=name)
  341. mbar.add_cascade(label=label, menu=menu, underline=underline)
  342. if macosxSupport.runningAsOSXApp():
  343. # Insert the application menu
  344. menudict['application'] = menu = Menu(mbar, name='apple')
  345. mbar.add_cascade(label='IDLE', menu=menu)
  346. self.fill_menus()
  347. self.base_helpmenu_length = self.menudict['help'].index(END)
  348. self.reset_help_menu_entries()
  349. def postwindowsmenu(self):
  350. # Only called when Windows menu exists
  351. menu = self.menudict['windows']
  352. end = menu.index("end")
  353. if end is None:
  354. end = -1
  355. if end > self.wmenu_end:
  356. menu.delete(self.wmenu_end+1, end)
  357. WindowList.add_windows_to_menu(menu)
  358. rmenu = None
  359. def right_menu_event(self, event):
  360. self.text.tag_remove("sel", "1.0", "end")
  361. self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
  362. if not self.rmenu:
  363. self.make_rmenu()
  364. rmenu = self.rmenu
  365. self.event = event
  366. iswin = sys.platform[:3] == 'win'
  367. if iswin:
  368. self.text.config(cursor="arrow")
  369. rmenu.tk_popup(event.x_root, event.y_root)
  370. if iswin:
  371. self.text.config(cursor="ibeam")
  372. rmenu_specs = [
  373. # ("Label", "<<virtual-event>>"), ...
  374. ("Close", "<<close-window>>"), # Example
  375. ]
  376. def make_rmenu(self):
  377. rmenu = Menu(self.text, tearoff=0)
  378. for label, eventname in self.rmenu_specs:
  379. def command(text=self.text, eventname=eventname):
  380. text.event_generate(eventname)
  381. rmenu.add_command(label=label, command=command)
  382. self.rmenu = rmenu
  383. def about_dialog(self, event=None):
  384. aboutDialog.AboutDialog(self.top,'About IDLE')
  385. def config_dialog(self, event=None):
  386. configDialog.ConfigDialog(self.top,'Settings')
  387. def help_dialog(self, event=None):
  388. fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
  389. textView.view_file(self.top,'Help',fn)
  390. def python_docs(self, event=None):
  391. if sys.platform[:3] == 'win':
  392. os.startfile(self.help_url)
  393. else:
  394. webbrowser.open(self.help_url)
  395. return "break"
  396. def cut(self,event):
  397. self.text.event_generate("<<Cut>>")
  398. return "break"
  399. def copy(self,event):
  400. if not self.text.tag_ranges("sel"):
  401. # There is no selection, so do nothing and maybe interrupt.
  402. return
  403. self.text.event_generate("<<Copy>>")
  404. return "break"
  405. def paste(self,event):
  406. self.text.event_generate("<<Paste>>")
  407. self.text.see("insert")
  408. return "break"
  409. def select_all(self, event=None):
  410. self.text.tag_add("sel", "1.0", "end-1c")
  411. self.text.mark_set("insert", "1.0")
  412. self.text.see("insert")
  413. return "break"
  414. def remove_selection(self, event=None):
  415. self.text.tag_remove("sel", "1.0", "end")
  416. self.text.see("insert")
  417. def move_at_edge_if_selection(self, edge_index):
  418. """Cursor move begins at start or end of selection
  419. When a left/right cursor key is pressed create and return to Tkinter a
  420. function which causes a cursor move from the associated edge of the
  421. selection.
  422. """
  423. self_text_index = self.text.index
  424. self_text_mark_set = self.text.mark_set
  425. edges_table = ("sel.first+1c", "sel.last-1c")
  426. def move_at_edge(event):
  427. if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
  428. try:
  429. self_text_index("sel.first")
  430. self_text_mark_set("insert", edges_table[edge_index])
  431. except TclError:
  432. pass
  433. return move_at_edge
  434. def del_word_left(self, event):
  435. self.text.event_generate('<Meta-Delete>')
  436. return "break"
  437. def del_word_right(self, event):
  438. self.text.event_generate('<Meta-d>')
  439. return "break"
  440. def find_event(self, event):
  441. SearchDialog.find(self.text)
  442. return "break"
  443. def find_again_event(self, event):
  444. SearchDialog.find_again(self.text)
  445. return "break"
  446. def find_selection_event(self, event):
  447. SearchDialog.find_selection(self.text)
  448. return "break"
  449. def find_in_files_event(self, event):
  450. GrepDialog.grep(self.text, self.io, self.flist)
  451. return "break"
  452. def replace_event(self, event):
  453. ReplaceDialog.replace(self.text)
  454. return "break"
  455. def goto_line_event(self, event):
  456. text = self.text
  457. lineno = tkSimpleDialog.askinteger("Goto",
  458. "Go to line number:",parent=text)
  459. if lineno is None:
  460. return "break"
  461. if lineno <= 0:
  462. text.bell()
  463. return "break"
  464. text.mark_set("insert", "%d.0" % lineno)
  465. text.see("insert")
  466. def open_module(self, event=None):
  467. # XXX Shouldn't this be in IOBinding or in FileList?
  468. try:
  469. name = self.text.get("sel.first", "sel.last")
  470. except TclError:
  471. name = ""
  472. else:
  473. name = name.strip()
  474. name = tkSimpleDialog.askstring("Module",
  475. "Enter the name of a Python module\n"
  476. "to search on sys.path and open:",
  477. parent=self.text, initialvalue=name)
  478. if name:
  479. name = name.strip()
  480. if not name:
  481. return
  482. # XXX Ought to insert current file's directory in front of path
  483. try:
  484. (f, file, (suffix, mode, type)) = _find_module(name)
  485. except (NameError, ImportError), msg:
  486. tkMessageBox.showerror("Import error", str(msg), parent=self.text)
  487. return
  488. if type != imp.PY_SOURCE:
  489. tkMessageBox.showerror("Unsupported type",
  490. "%s is not a source module" % name, parent=self.text)
  491. return
  492. if f:
  493. f.close()
  494. if self.flist:
  495. self.flist.open(file)
  496. else:
  497. self.io.loadfile(file)
  498. def open_class_browser(self, event=None):
  499. filename = self.io.filename
  500. if not filename:
  501. tkMessageBox.showerror(
  502. "No filename",
  503. "This buffer has no associated filename",
  504. master=self.text)
  505. self.text.focus_set()
  506. return None
  507. head, tail = os.path.split(filename)
  508. base, ext = os.path.splitext(tail)
  509. import ClassBrowser
  510. ClassBrowser.ClassBrowser(self.flist, base, [head])
  511. def open_path_browser(self, event=None):
  512. import PathBrowser
  513. PathBrowser.PathBrowser(self.flist)
  514. def gotoline(self, lineno):
  515. if lineno is not None and lineno > 0:
  516. self.text.mark_set("insert", "%d.0" % lineno)
  517. self.text.tag_remove("sel", "1.0", "end")
  518. self.text.tag_add("sel", "insert", "insert +1l")
  519. self.center()
  520. def ispythonsource(self, filename):
  521. if not filename or os.path.isdir(filename):
  522. return True
  523. base, ext = os.path.splitext(os.path.basename(filename))
  524. if os.path.normcase(ext) in (".py", ".pyw"):
  525. return True
  526. try:
  527. f = open(filename)
  528. line = f.readline()
  529. f.close()
  530. except IOError:
  531. return False
  532. return line.startswith('#!') and line.find('python') >= 0
  533. def close_hook(self):
  534. if self.flist:
  535. self.flist.unregister_maybe_terminate(self)
  536. self.flist = None
  537. def set_close_hook(self, close_hook):
  538. self.close_hook = close_hook
  539. def filename_change_hook(self):
  540. if self.flist:
  541. self.flist.filename_changed_edit(self)
  542. self.saved_change_hook()
  543. self.top.update_windowlist_registry(self)
  544. self.ResetColorizer()
  545. def _addcolorizer(self):
  546. if self.color:
  547. return
  548. if self.ispythonsource(self.io.filename):
  549. self.color = self.ColorDelegator()
  550. # can add more colorizers here...
  551. if self.color:
  552. self.per.removefilter(self.undo)
  553. self.per.insertfilter(self.color)
  554. self.per.insertfilter(self.undo)
  555. def _rmcolorizer(self):
  556. if not self.color:
  557. return
  558. self.color.removecolors()
  559. self.per.removefilter(self.color)
  560. self.color = None
  561. def ResetColorizer(self):
  562. "Update the colour theme"
  563. # Called from self.filename_change_hook and from configDialog.py
  564. self._rmcolorizer()
  565. self._addcolorizer()
  566. theme = idleConf.GetOption('main','Theme','name')
  567. normal_colors = idleConf.GetHighlight(theme, 'normal')
  568. cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
  569. select_colors = idleConf.GetHighlight(theme, 'hilite')
  570. self.text.config(
  571. foreground=normal_colors['foreground'],
  572. background=normal_colors['background'],
  573. insertbackground=cursor_color,
  574. selectforeground=select_colors['foreground'],
  575. selectbackground=select_colors['background'],
  576. )
  577. def ResetFont(self):
  578. "Update the text widgets' font if it is changed"
  579. # Called from configDialog.py
  580. fontWeight='normal'
  581. if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
  582. fontWeight='bold'
  583. self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
  584. idleConf.GetOption('main','EditorWindow','font-size'),
  585. fontWeight))
  586. def RemoveKeybindings(self):
  587. "Remove the keybindings before they are changed."
  588. # Called from configDialog.py
  589. self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
  590. for event, keylist in keydefs.items():
  591. self.text.event_delete(event, *keylist)
  592. for extensionName in self.get_standard_extension_names():
  593. xkeydefs = idleConf.GetExtensionBindings(extensionName)
  594. if xkeydefs:
  595. for event, keylist in xkeydefs.items():
  596. self.text.event_delete(event, *keylist)
  597. def ApplyKeybindings(self):
  598. "Update the keybindings after they are changed"
  599. # Called from configDialog.py
  600. self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
  601. self.apply_bindings()
  602. for extensionName in self.get_standard_extension_names():
  603. xkeydefs = idleConf.GetExtensionBindings(extensionName)
  604. if xkeydefs:
  605. self.apply_bindings(xkeydefs)
  606. #update menu accelerators
  607. menuEventDict = {}
  608. for menu in self.Bindings.menudefs:
  609. menuEventDict[menu[0]] = {}
  610. for item in menu[1]:
  611. if item:
  612. menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
  613. for menubarItem in self.menudict.keys():
  614. menu = self.menudict[menubarItem]
  615. end = menu.index(END) + 1
  616. for index in range(0, end):
  617. if menu.type(index) == 'command':
  618. accel = menu.entrycget(index, 'accelerator')
  619. if accel:
  620. itemName = menu.entrycget(index, 'label')
  621. event = ''
  622. if menuEventDict.has_key(menubarItem):
  623. if menuEventDict[menubarItem].has_key(itemName):
  624. event = menuEventDict[menubarItem][itemName]
  625. if event:
  626. accel = get_accelerator(keydefs, event)
  627. menu.entryconfig(index, accelerator=accel)
  628. def set_notabs_indentwidth(self):
  629. "Update the indentwidth if changed and not using tabs in this window"
  630. # Called from configDialog.py
  631. if not self.usetabs:
  632. self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
  633. type='int')
  634. def reset_help_menu_entries(self):
  635. "Update the additional help entries on the Help menu"
  636. help_list = idleConf.GetAllExtraHelpSourcesList()
  637. helpmenu = self.menudict['help']
  638. # first delete the extra help entries, if any
  639. helpmenu_length = helpmenu.index(END)
  640. if helpmenu_length > self.base_helpmenu_length:
  641. helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
  642. # then rebuild them
  643. if help_list:
  644. helpmenu.add_separator()
  645. for entry in help_list:
  646. cmd = self.__extra_help_callback(entry[1])
  647. helpmenu.add_command(label=entry[0], command=cmd)
  648. # and update the menu dictionary
  649. self.menudict['help'] = helpmenu
  650. def __extra_help_callback(self, helpfile):
  651. "Create a callback with the helpfile value frozen at definition time"
  652. def display_extra_help(helpfile=helpfile):
  653. if not helpfile.startswith(('www', 'http')):
  654. url = os.path.normpath(helpfile)
  655. if sys.platform[:3] == 'win':
  656. os.startfile(helpfile)
  657. else:
  658. webbrowser.open(helpfile)
  659. return display_extra_help
  660. def update_recent_files_list(self, new_file=None):
  661. "Load and update the recent files list and menus"
  662. rf_list = []
  663. if os.path.exists(self.recent_files_path):
  664. rf_list_file = open(self.recent_files_path,'r')
  665. try:
  666. rf_list = rf_list_file.readlines()
  667. finally:
  668. rf_list_file.close()
  669. if new_file:
  670. new_file = os.path.abspath(new_file) + '\n'
  671. if new_file in rf_list:
  672. rf_list.remove(new_file) # move to top
  673. rf_list.insert(0, new_file)
  674. # clean and save the recent files list
  675. bad_paths = []
  676. for path in rf_list:
  677. if '\0' in path or not os.path.exists(path[0:-1]):
  678. bad_paths.append(path)
  679. rf_list = [path for path in rf_list if path not in bad_paths]
  680. ulchars = "1234567890ABCDEFGHIJK"
  681. rf_list = rf_list[0:len(ulchars)]
  682. rf_file = open(self.recent_files_path, 'w')
  683. try:
  684. rf_file.writelines(rf_list)
  685. finally:
  686. rf_file.close()
  687. # for each edit window instance, construct the recent files menu
  688. for instance in self.top.instance_dict.keys():
  689. menu = instance.recent_files_menu
  690. menu.delete(1, END) # clear, and rebuild:
  691. for i, file in zip(count(), rf_list):
  692. file_name = file[0:-1] # zap \n
  693. # make unicode string to display non-ASCII chars correctly
  694. ufile_name = self._filename_to_unicode(file_name)
  695. callback = instance.__recent_file_callback(file_name)
  696. menu.add_command(label=ulchars[i] + " " + ufile_name,
  697. command=callback,
  698. underline=0)
  699. def __recent_file_callback(self, file_name):
  700. def open_recent_file(fn_closure=file_name):
  701. self.io.open(editFile=fn_closure)
  702. return open_recent_file
  703. def saved_change_hook(self):
  704. short = self.short_title()
  705. long = self.long_title()
  706. if short and long:
  707. title = short + " - " + long
  708. elif short:
  709. title = short
  710. elif long:
  711. title = long
  712. else:
  713. title = "Untitled"
  714. icon = short or long or title
  715. if not self.get_saved():
  716. title = "*%s*" % title
  717. icon = "*%s" % icon
  718. self.top.wm_title(title)
  719. self.top.wm_iconname(icon)
  720. def get_saved(self):
  721. return self.undo.get_saved()
  722. def set_saved(self, flag):
  723. self.undo.set_saved(flag)
  724. def reset_undo(self):
  725. self.undo.reset_undo()
  726. def short_title(self):
  727. filename = self.io.filename
  728. if filename:
  729. filename = os.path.basename(filename)
  730. # return unicode string to display non-ASCII chars correctly
  731. return self._filename_to_unicode(filename)
  732. def long_title(self):
  733. # return unicode string to display non-ASCII chars correctly
  734. return self._filename_to_unicode(self.io.filename or "")
  735. def center_insert_event(self, event):
  736. self.center()
  737. def center(self, mark="insert"):
  738. text = self.text
  739. top, bot = self.getwindowlines()
  740. lineno = self.getlineno(mark)
  741. height = bot - top
  742. newtop = max(1, lineno - height//2)
  743. text.yview(float(newtop))
  744. def getwindowlines(self):
  745. text = self.text
  746. top = self.getlineno("@0,0")
  747. bot = self.getlineno("@0,65535")
  748. if top == bot and text.winfo_height() == 1:
  749. # Geometry manager hasn't run yet
  750. height = int(text['height'])
  751. bot = top + height - 1
  752. return top, bot
  753. def getlineno(self, mark="insert"):
  754. text = self.text
  755. return int(float(text.index(mark)))
  756. def get_geometry(self):
  757. "Return (width, height, x, y)"
  758. geom = self.top.wm_geometry()
  759. m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
  760. tuple = (map(int, m.groups()))
  761. return tuple
  762. def close_event(self, event):
  763. self.close()
  764. def maybesave(self):
  765. if self.io:
  766. if not self.get_saved():
  767. if self.top.state()!='normal':
  768. self.top.deiconify()
  769. self.top.lower()
  770. self.top.lift()
  771. return self.io.maybesave()
  772. def close(self):
  773. reply = self.maybesave()
  774. if str(reply) != "cancel":
  775. self._close()
  776. return reply
  777. def _close(self):
  778. if self.io.filename:
  779. self.update_recent_files_list(new_file=self.io.filename)
  780. WindowList.unregister_callback(self.postwindowsmenu)
  781. self.unload_extensions()
  782. self.io.close()
  783. self.io = None
  784. self.undo = None
  785. if self.color:
  786. self.color.close(False)
  787. self.color = None
  788. self.text = None
  789. self.tkinter_vars = None
  790. self.per.close()
  791. self.per = None
  792. self.top.destroy()
  793. if self.close_hook:
  794. # unless override: unregister from flist, terminate if last window
  795. self.close_hook()
  796. def load_extensions(self):
  797. self.extensions = {}
  798. self.load_standard_extensions()
  799. def unload_extensions(self):
  800. for ins in self.extensions.values():
  801. if hasattr(ins, "close"):
  802. ins.close()
  803. self.extensions = {}
  804. def load_standard_extensions(self):
  805. for name in self.get_standard_extension_names():
  806. try:
  807. self.load_extension(name)
  808. except:
  809. print "Failed to load extension", repr(name)
  810. import traceback
  811. traceback.print_exc()
  812. def get_standard_extension_names(self):
  813. return idleConf.GetExtensions(editor_only=True)
  814. def load_extension(self, name):
  815. try:
  816. mod = __import__(name, globals(), locals(), [])
  817. except ImportError:
  818. print "\nFailed to import extension: ", name
  819. return
  820. cls = getattr(mod, name)
  821. keydefs = idleConf.GetExtensionBindings(name)
  822. if hasattr(cls, "menudefs"):
  823. self.fill_menus(cls.menudefs, keydefs)
  824. ins = cls(self)
  825. self.extensions[name] = ins
  826. if keydefs:
  827. self.apply_bindings(keydefs)
  828. for vevent in keydefs.keys():
  829. methodname = vevent.replace("-", "_")
  830. while methodname[:1] == '<':
  831. methodname = methodname[1:]
  832. while methodname[-1:] == '>':
  833. methodname = methodname[:-1]
  834. methodname = methodname + "_event"
  835. if hasattr(ins, methodname):
  836. self.text.bind(vevent, getattr(ins, methodname))
  837. def apply_bindings(self, keydefs=None):
  838. if keydefs is None:
  839. keydefs = self.Bindings.default_keydefs
  840. text = self.text
  841. text.keydefs = keydefs
  842. for event, keylist in keydefs.items():
  843. if keylist:
  844. text.event_add(event, *keylist)
  845. def fill_menus(self, menudefs=None, keydefs=None):
  846. """Add appropriate entries to the menus and submenus
  847. Menus that are absent or None in self.menudict are ignored.
  848. """
  849. if menudefs is None:
  850. menudefs = self.Bindings.menudefs
  851. if keydefs is None:
  852. keydefs = self.Bindings.default_keydefs
  853. menudict = self.menudict
  854. text = self.text
  855. for mname, entrylist in menudefs:
  856. menu = menudict.get(mname)
  857. if not menu:
  858. continue
  859. for entry in entrylist:
  860. if not entry:
  861. menu.add_separator()
  862. else:
  863. label, eventname = entry
  864. checkbutton = (label[:1] == '!')
  865. if checkbutton:
  866. label = label[1:]
  867. underline, label = prepstr(label)
  868. accelerator = get_accelerator(keydefs, eventname)
  869. def command(text=text, eventname=eventname):
  870. text.event_generate(eventname)
  871. if checkbutton:
  872. var = self.get_var_obj(eventname, BooleanVar)
  873. menu.add_checkbutton(label=label, underline=underline,
  874. command=command, accelerator=accelerator,
  875. variable=var)
  876. else:
  877. menu.add_command(label=label, underline=underline,
  878. command=command,
  879. accelerator=accelerator)
  880. def getvar(self, name):
  881. var = self.get_var_obj(name)
  882. if var:
  883. value = var.get()
  884. return value
  885. else:
  886. raise NameError, name
  887. def setvar(self, name, value, vartype=None):
  888. var = self.get_var_obj(name, vartype)
  889. if var:
  890. var.set(value)
  891. else:
  892. raise NameError, name
  893. def get_var_obj(self, name, vartype=None):
  894. var = self.tkinter_vars.get(name)
  895. if not var and vartype:
  896. # create a Tkinter variable object with self.text as master:
  897. self.tkinter_vars[name] = var = vartype(self.text)
  898. return var
  899. # Tk implementations of "virtual text methods" -- each platform
  900. # reusing IDLE's support code needs to define these for its GUI's
  901. # flavor of widget.
  902. # Is character at text_index in a Python string? Return 0 for
  903. # "guaranteed no", true for anything else. This info is expensive
  904. # to compute ab initio, but is probably already known by the
  905. # platform's colorizer.
  906. def is_char_in_string(self, text_index):
  907. if self.color:
  908. # Return true iff colorizer hasn't (re)gotten this far
  909. # yet, or the character is tagged as being in a string
  910. return self.text.tag_prevrange("TODO", text_index) or \
  911. "STRING" in self.text.tag_names(text_index)
  912. else:
  913. # The colorizer is missing: assume the worst
  914. return 1
  915. # If a selection is defined in the text widget, return (start,
  916. # end) as Tkinter text indices, otherwise return (None, None)
  917. def get_selection_indices(self):
  918. try:
  919. first = self.text.index("sel.first")
  920. last = self.text.index("sel.last")
  921. return first, last
  922. except TclError:
  923. return None, None
  924. # Return the text widget's current view of what a tab stop means
  925. # (equivalent width in spaces).
  926. def get_tabwidth(self):
  927. current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
  928. return int(current)
  929. # Set the text widget's current view of what a tab stop means.
  930. def set_tabwidth(self, newtabwidth):
  931. text = self.text
  932. if self.get_tabwidth() != newtabwidth:
  933. pixels = text.tk.call("font", "measure", text["font"],
  934. "-displayof", text.master,
  935. "n" * newtabwidth)
  936. text.configure(tabs=pixels)
  937. # If ispythonsource and guess are true, guess a good value for
  938. # indentwidth based on file content (if possible), and if
  939. # indentwidth != tabwidth set usetabs false.
  940. # In any case, adjust the Text widget's view of what a tab
  941. # character means.
  942. def set_indentation_params(self, ispythonsource, guess=True):
  943. if guess and ispythonsource:
  944. i = self.guess_indent()
  945. if 2 <= i <= 8:
  946. self.indentwidth = i
  947. if self.indentwidth != self.tabwidth:
  948. self.usetabs = False
  949. self.set_tabwidth(self.tabwidth)
  950. def smart_backspace_event(self, event):
  951. text = self.text
  952. first, last = self.get_selection_indices()
  953. if first and last:
  954. text.delete(first, last)
  955. text.mark_set("insert", first)
  956. return "break"
  957. # Delete whitespace left, until hitting a real char or closest
  958. # preceding virtual tab stop.
  959. chars = text.get("insert linestart", "insert")
  960. if chars == '':
  961. if text.compare("insert", ">", "1.0"):
  962. # easy: delete preceding newline
  963. text.delete("insert-1c")
  964. else:
  965. text.bell() # at start of buffer
  966. return "break"
  967. if chars[-1] not in " \t":
  968. # easy: delete preceding real char
  969. text.delete("insert-1c")
  970. return "break"
  971. # Ick. It may require *inserting* spaces if we back up over a
  972. # tab character! This is written to be clear, not fast.
  973. tabwidth = self.tabwidth
  974. have = len(chars.expandtabs(tabwidth))
  975. assert have > 0
  976. want = ((have - 1) // self.indentwidth) * self.indentwidth
  977. # Debug prompt is multilined....
  978. last_line_of_prompt = sys.ps1.split('\n')[-1]
  979. ncharsdeleted = 0
  980. while 1:
  981. if chars == last_line_of_prompt:
  982. break
  983. chars = chars[:-1]
  984. ncharsdeleted = ncharsdeleted + 1
  985. have = len(chars.expandtabs(tabwidth))
  986. if have <= want or chars[-1] not in " \t":
  987. break
  988. text.undo_block_start()
  989. text.delete("insert-%dc" % ncharsdeleted, "insert")
  990. if have < want:
  991. text.insert("insert", ' ' * (want - have))
  992. text.undo_block_stop()
  993. return "break"
  994. def smart_indent_event(self, event):
  995. # if intraline selection:
  996. # delete it
  997. # elif multiline selection:
  998. # do indent-region
  999. # else:
  1000. # indent one level
  1001. text = self.text
  1002. first, last = self.get_selection_indices()
  1003. text.undo_block_start()
  1004. try:
  1005. if first and last:
  1006. if index2line(first) != index2line(last):
  1007. return self.indent_region_event(event)
  1008. text.delete(first, last)
  1009. text.mark_set("insert", first)
  1010. prefix = text.get("insert linestart", "insert")
  1011. raw, effective = classifyws(prefix, self.tabwidth)
  1012. if raw == len(prefix):
  1013. # only whitespace to the left
  1014. self.reindent_to(effective + self.indentwidth)
  1015. else:
  1016. # tab to the next 'stop' within or to right of line's text:
  1017. if self.usetabs:
  1018. pad = '\t'
  1019. else:
  1020. effective = len(prefix.expandtabs(self.tabwidth))
  1021. n = self.indentwidth
  1022. pad = ' ' * (n - effective % n)
  1023. text.insert("insert", pad)
  1024. text.see("insert")
  1025. return "break"
  1026. finally:
  1027. text.undo_block_stop()
  1028. def newline_and_indent_event(self, event):
  1029. text = self.text
  1030. first, last = self.get_selection_indices()
  1031. text.undo_block_start()
  1032. try:
  1033. if first and last:
  1034. text.delete(first, last)
  1035. text.mark_set("insert", first)
  1036. line = text.get("insert linestart", "insert")
  1037. i, n = 0, len(line)
  1038. while i < n and line[i] in " \t":
  1039. i = i+1
  1040. if i == n:
  1041. # the cursor is in or at leading indentation in a continuation
  1042. # line; just inject an empty line at the start
  1043. text.insert("insert linestart", '\n')
  1044. return "break"
  1045. indent = line[:i]
  1046. # strip whitespace before insert point unless it's in the prompt
  1047. i = 0
  1048. last_line_of_prompt = sys.ps1.split('\n')[-1]
  1049. while line and line[-1] in " \t" and line != last_line_of_prompt:
  1050. line = line[:-1]
  1051. i = i+1
  1052. if i:
  1053. text.delete("insert - %d chars" % i, "insert")
  1054. # strip whitespace after insert point
  1055. while text.get("insert") in " \t":
  1056. text.delete("insert")
  1057. # start new line
  1058. text.insert("insert", '\n')
  1059. # adjust indentation for continuations and block
  1060. # open/close first need to find the last stmt
  1061. lno = index2line(text.index('insert'))
  1062. y = PyParse.Parser(self.indentwidth, self.tabwidth)
  1063. if not self.context_use_ps1:
  1064. for context in self.num_context_lines:
  1065. startat = max(lno - context, 1)
  1066. startatindex = `startat` + ".0"
  1067. rawtext = text.get(startatindex, "insert")
  1068. y.set_str(rawtext)
  1069. bod = y.find_good_parse_start(
  1070. self.context_use_ps1,
  1071. self._build_char_in_string_func(startatindex))
  1072. if bod is not None or startat == 1:
  1073. break
  1074. y.set_lo(bod or 0)
  1075. else:
  1076. r = text.tag_prevrange("console", "insert")
  1077. if r:
  1078. startatindex = r[1]
  1079. else:
  1080. startatindex = "1.0"
  1081. rawtext = text.get(startatindex, "insert")
  1082. y.set_str(rawtext)
  1083. y.set_lo(0)
  1084. c = y.get_continuation_type()
  1085. if c != PyParse.C_NONE:
  1086. # The current stmt hasn't ended yet.
  1087. if c == PyParse.C_STRING_FIRST_LINE:
  1088. # after the first line of a string; do not indent at all
  1089. pass
  1090. elif c == PyParse.C_STRING_NEXT_LINES:
  1091. # inside a string which started before this line;
  1092. # just mimic the current indent
  1093. text.insert("insert", indent)
  1094. elif c == PyParse.C_BRACKET:
  1095. # line up with the first (if any) element of the
  1096. # last open bracket structure; else indent one
  1097. # level beyond the indent of the line with the
  1098. # last open bracket
  1099. self.reindent_to(y.compute_bracket_indent())
  1100. elif c == PyParse.C_BACKSLASH:
  1101. # if more than one line in this stmt already, just
  1102. # mimic the current indent; else if initial line
  1103. # has a start on an assignment stmt, indent to
  1104. # beyond leftmost =; else to beyond first chunk of
  1105. # non-whitespace on initial line
  1106. if y.get_num_lines_in_stmt() > 1:
  1107. text.insert("insert", indent)
  1108. else:
  1109. self.reindent_to(y.compute_backslash_indent())
  1110. else:
  1111. assert 0, "bogus continuation type %r" % (c,)
  1112. return "break"
  1113. # This line starts a brand new stmt; indent relative to
  1114. # indentation of initial line of closest preceding
  1115. # interesting stmt.
  1116. indent = y.get_base_indent_string()
  1117. text.insert("insert", indent)
  1118. if y.is_block_opener():
  1119. self.smart_indent_event(event)
  1120. elif indent and y.is_block_closer():
  1121. self.smart_backspace_event(event)
  1122. return "break"
  1123. finally:
  1124. text.see("insert")
  1125. text.undo_block_stop()
  1126. # Our editwin provides a is_char_in_string function that works
  1127. # with a Tk text index, but PyParse only knows about offsets into
  1128. # a string. This builds a function for PyParse that accepts an
  1129. # offset.
  1130. def _build_char_in_string_func(self, startindex):
  1131. def inner(offset, _startindex=startindex,
  1132. _icis=self.is_char_in_string):
  1133. return _icis(_startindex + "+%dc" % offset)
  1134. return inner
  1135. def indent_region_event(self, event):
  1136. head, tail, chars, lines = self.get_region()
  1137. for pos in range(len(lines)):
  1138. line = lines[pos]
  1139. if line:
  1140. raw, effective = classifyws(line, self.tabwidth)
  1141. effective = effective + self.indentwidth
  1142. lines[pos] = self._make_blanks(effective) + line[raw:]
  1143. self.set_region(head, tail, chars, lines)
  1144. return "break"
  1145. def dedent_region_event(self, event):
  1146. head, tail, chars, lines = self.get_region()
  1147. for pos in range(len(lines)):
  1148. line = lines[pos]
  1149. if line:
  1150. raw, effective = classifyws(line, self.tabwidth)
  1151. effective = max(effective - self.indentwidth, 0)
  1152. lines[pos] = self._make_blanks(effective) + line[raw:]
  1153. self.set_region(head, tail, chars, lines)
  1154. return "break"
  1155. def comment_region_event(self, event):
  1156. head, tail, chars, lines = self.get_region()
  1157. for pos in range(len(lines) - 1):
  1158. line = lines[pos]
  1159. lines[pos] = '##' + line
  1160. self.set_region(head, tail, chars, lines)
  1161. def uncomment_region_event(self, event):
  1162. head, tail, chars, lines = self.get_region()
  1163. for pos in range(len(lines)):
  1164. line = lines[pos]
  1165. if not line:
  1166. continue
  1167. if line[:2] == '##':
  1168. line = line[2:]
  1169. elif line[:1] == '#':
  1170. line = line[1:]
  1171. lines[pos] = line
  1172. self.set_region(head, tail, chars, lines)
  1173. def tabify_region_event(self, event):
  1174. head, tail, chars, lines = self.get_region()
  1175. tabwidth = self._asktabwidth()
  1176. for pos in range(len(lines)):
  1177. line = lines[pos]
  1178. if line:
  1179. raw, effective = classifyws(line, tabwidth)
  1180. ntabs, nspaces = divmod(effective, tabwidth)
  1181. lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
  1182. self.set_region(head, tail, chars, lines)
  1183. def untabify_region_event(self, event):
  1184. head, tail, chars, lines = self.get_region()
  1185. tabwidth = self._asktabwidth()
  1186. for pos in range(len(lines)):
  1187. lines[pos] = lines[pos].expandtabs(tabwidth)
  1188. self.set_region(head, tail, chars, lines)
  1189. def toggle_tabs_event(self, event):
  1190. if self.askyesno(
  1191. "Toggle tabs",
  1192. "Turn tabs " + ("on", "off")[self.usetabs] +
  1193. "?\nIndent width " +
  1194. ("will be", "remains at")[self.usetabs] + " 8." +
  1195. "\n Note: a tab is always 8 columns",
  1196. parent=self.text):
  1197. self.usetabs = not self.usetabs
  1198. # Try to prevent inconsistent indentation.
  1199. # User must change indent width manually after using tabs.
  1200. self.indentwidth = 8
  1201. return "break"
  1202. # XXX this isn't bound to anything -- see tabwidth comments
  1203. ## def change_tabwidth_event(self, event):
  1204. ## new = self._asktabwidth()
  1205. ## if new != self.tabwidth:
  1206. ## self.tabwidth = new
  1207. ## self.set_indentation_params(0, guess=0)
  1208. ## return "break"
  1209. def change_indentwidth_event(self, event):
  1210. new = self.askinteger(
  1211. "Indent width",
  1212. "New indent width (2-16)\n(Always use 8 when using tabs)",
  1213. parent=self.text,
  1214. initialvalue=self.indentwidth,
  1215. minvalue=2,
  1216. maxvalue=16)
  1217. if new and new != self.indentwidth and not self.usetabs:
  1218. self.indentwidth = new
  1219. return "break"
  1220. def get_region(self):
  1221. text = self.text
  1222. first, last = self.get_selection_indices()
  1223. if first and last:
  1224. head = text.index(first + " linestart")
  1225. tail = text.index(last + "-1c lineend +1c")
  1226. else:
  1227. head = text.index("insert linestart")
  1228. tail = text.index("insert lineend +1c")
  1229. chars = text.get(head, tail)
  1230. lines = chars.split("\n")
  1231. return head, tail, chars, lines
  1232. def set_region(self, head, tail, chars, lines):
  1233. text = self.text
  1234. newchars = "\n".join(lines)
  1235. if newchars == chars:
  1236. text.bell()
  1237. return
  1238. text.tag_remove("sel", "1.0", "end")
  1239. text.mark_set("insert", head)
  1240. text.undo_block_start()
  1241. text.delete(head, tail)
  1242. text.insert(head, newchars)
  1243. text.undo_block_stop()
  1244. text.tag_add("sel", head, "insert")
  1245. # Make string that displays as n leading blanks.
  1246. def _make_blanks(self, n):
  1247. if self.usetabs:
  1248. ntabs, nspaces = divmod(n, self.tabwidth)
  1249. return '\t' * ntabs + ' ' * nspaces
  1250. else:
  1251. return ' ' * n
  1252. # Delete from beginning of line to insert point, then reinsert
  1253. # column logical (meaning use tabs if appropriate) spaces.
  1254. def reindent_to(self, column):
  1255. text = self.text
  1256. text.undo_block_start()
  1257. if text.compare("insert linestart", "!=", "insert"):
  1258. text.delete("insert linestart", "insert")
  1259. if column:
  1260. text.insert("insert", self._make_blanks(column))
  1261. text.undo_block_stop()
  1262. def _asktabwidth(self):
  1263. return self.askinteger(
  1264. "Tab width",
  1265. "Columns per tab? (2-16)",
  1266. parent=self.text,
  1267. initialvalue=self.indentwidth,
  1268. minvalue=2,
  1269. maxvalue=16) or self.tabwidth
  1270. # Guess indentwidth from text content.
  1271. # Return guessed indentwidth. This should not be believed unless
  1272. # it's in a reasonable range (e.g., it will be 0 if no indented
  1273. # blocks are found).
  1274. def guess_indent(self):
  1275. opener, indented = IndentSearcher(self.text, self.tabwidth).run()
  1276. if opener and indented:
  1277. raw, indentsmall = classifyws(opener, self.tabwidth)
  1278. raw, indentlarge = classifyws(indented, self.tabwidth)
  1279. else:
  1280. indentsmall = indentlarge = 0
  1281. return indentlarge - indentsmall
  1282. # "line.col" -> line, as an int
  1283. def index2line(index):
  1284. return int(float(index))
  1285. # Look at the leading whitespace in s.
  1286. # Return pair (# of leading ws characters,
  1287. # effective # of leading blanks after expanding
  1288. # tabs to width tabwidth)
  1289. def classifyws(s, tabwidth):
  1290. raw = effective = 0
  1291. for ch in s:
  1292. if ch == ' ':
  1293. raw = raw + 1
  1294. effective = effective + 1
  1295. elif ch == '\t':
  1296. raw = raw + 1
  1297. effective = (effective // tabwidth + 1) * tabwidth
  1298. else:
  1299. break
  1300. return raw, effective
  1301. import tokenize
  1302. _tokenize = tokenize
  1303. del tokenize
  1304. class IndentSearcher(object):
  1305. # .run() chews over the Text widget, looking for a block opener
  1306. # and the stmt following it. Returns a pair,
  1307. # (line containing block opener, line containing stmt)
  1308. # Either or both may be None.
  1309. def __init__(self, text, tabwidth):
  1310. self.text = text
  1311. self.tabwidth = tabwidth
  1312. self.i = self.finished = 0
  1313. self.blkopenline = self.indentedline = None
  1314. def readline(self):
  1315. if self.finished:
  1316. return ""
  1317. i = self.i = self.i + 1
  1318. mark = repr(i) + ".0"
  1319. if self.text.compare(mark, ">=", "end"):
  1320. return ""
  1321. return self.text.get(mark, mark + " lineend+1c")
  1322. def tokeneater(self, type, token, start, end, line,
  1323. INDENT=_tokenize.INDENT,
  1324. NAME=_tokenize.NAME,
  1325. OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
  1326. if self.finished:
  1327. pass
  1328. elif type == NAME and token in OPENERS:
  1329. self.blkopenline = line
  1330. elif type == INDENT and self.blkopenline:
  1331. self.indentedline = line
  1332. self.finished = 1
  1333. def run(self):
  1334. save_tabsize = _tokenize.tabsize
  1335. _tokenize.tabsize = self.tabwidth
  1336. try:
  1337. try:
  1338. _tokenize.tokenize(self.readline, self.tokeneater)
  1339. except _tokenize.TokenError:
  1340. # since we cut off the tokenizer early, we can trigger
  1341. # spurious errors
  1342. pass
  1343. finally:
  1344. _tokenize.tabsize = save_tabsize
  1345. return self.blkopenline, self.indentedline
  1346. ### end autoindent code ###
  1347. def prepstr(s):
  1348. # Helper to extract the underscore from a string, e.g.
  1349. # prepstr("Co_py") returns (2, "Copy").
  1350. i = s.find('_')
  1351. if i >= 0:
  1352. s = s[:i] + s[i+1:]
  1353. return i, s
  1354. keynames = {
  1355. 'bracketleft': '[',
  1356. 'bracketright': ']',
  1357. 'slash': '/',
  1358. }
  1359. def get_accelerator(keydefs, eventname):
  1360. keylist = keydefs.get(eventname)
  1361. if not keylist:
  1362. return ""
  1363. s = keylist[0]
  1364. s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
  1365. s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
  1366. s = re.sub("Key-", "", s)
  1367. s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
  1368. s = re.sub("Control-", "Ctrl-", s)
  1369. s = re.sub("-", "+", s)
  1370. s = re.sub("><", " ", s)
  1371. s = re.sub("<", "", s)
  1372. s = re.sub(">", "", s)
  1373. return s
  1374. def fixwordbreaks(root):
  1375. # Make sure that Tk's double-click and next/previous word
  1376. # operations use our definition of a word (i.e. an identifier)
  1377. tk = root.tk
  1378. tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
  1379. tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
  1380. tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
  1381. def test():
  1382. root = Tk()
  1383. fixwordbreaks(root)
  1384. root.withdraw()
  1385. if sys.argv[1:]:
  1386. filename = sys.argv[1]
  1387. else:
  1388. filename = None
  1389. edit = EditorWindow(root=root, filename=filename)
  1390. edit.set_close_hook(root.quit)
  1391. edit.text.bind("<<close-all-windows>>", edit.close_event)
  1392. root.mainloop()
  1393. root.destroy()
  1394. if __name__ == '__main__':
  1395. test()