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