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