~malept/ubuntu/lucid/python2.6/dev-dependency-fix

« back to all changes in this revision

Viewing changes to Lib/idlelib/EditorWindow.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-02-13 12:51:00 UTC
  • Revision ID: james.westby@ubuntu.com-20090213125100-uufgcb9yeqzujpqw
Tags: upstream-2.6.1
ImportĀ upstreamĀ versionĀ 2.6.1

Show diffs side-by-side

added added

removed removed

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