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