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