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