5
from itertools import count
9
from MultiCall import MultiCallCreator
18
from configHandler import idleConf
19
import aboutDialog, textView, configDialog
22
# The default tab setting for a Text widget, in average-width characters.
23
TK_TABWIDTH_DEFAULT = 8
25
def _find_module(fullname, path=None):
26
"""Version of imp.find_module() that handles hierarchical module names"""
29
for tgt in fullname.split('.'):
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)
37
path = module.__path__
38
except AttributeError:
39
raise ImportError, 'No source for module ' + module.__name__
40
return file, filename, descr
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
48
from Tkinter import Toplevel
49
from MultiStatusBar import MultiStatusBar
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'
62
basepath = '/usr/share/doc/' # standard location
63
dochome = os.path.join(basepath, pyver,
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):
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')
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
83
EditorWindow.help_url = "http://www.python.org/doc/current"
84
currentTheme=idleConf.CurrentTheme()
86
root = root or flist.root
90
except AttributeError:
92
self.menubar = Menu(root)
93
self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
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
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(),
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',
111
height=idleConf.GetOption('main','EditorWindow','height') )
112
self.top.focused_widget = self.text
115
self.apply_bindings()
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)
159
flist.inversedict[self] = 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)
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'):
174
text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
175
idleConf.GetOption('main', 'EditorWindow', 'font-size'),
177
text_frame.pack(side=LEFT, fill=BOTH, expand=1)
178
text.pack(side=TOP, fill=BOTH, expand=1)
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
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.
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()
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
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
214
# Making the initial values larger slows things down more often.
215
self.num_context_lines = 50, 500, 5000000
217
self.per = per = self.Percolator(text)
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)
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)
229
# Create the recent files submenu
230
self.recent_files_menu = Menu(self.menubar)
231
self.menudict['file'].insert_cascade(3, label='Recent Files',
233
menu=self.recent_files_menu)
234
self.update_recent_files_list()
236
self.color = None # initialized below in self.ResetColorizer
238
if os.path.exists(filename) and not os.path.isdir(filename):
239
io.loadfile(filename)
241
io.set_filename(filename)
242
self.ResetColorizer()
243
self.saved_change_hook()
245
self.set_indentation_params(self.ispythonsource(filename))
247
self.load_extensions()
249
menu = self.menudict.get('windows')
251
end = menu.index("end")
258
WindowList.register_callback(self.postwindowsmenu)
260
# Some abstractions so IDLE extensions are cross-IDE
261
self.askyesno = tkMessageBox.askyesno
262
self.askinteger = tkSimpleDialog.askinteger
263
self.showerror = tkMessageBox.showerror
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:
271
return filename.decode(self.filesystemencoding)
272
except UnicodeDecodeError:
275
return filename.decode(self.encoding)
276
except UnicodeDecodeError:
277
# byte-to-byte conversion
278
return filename.decode('iso8859-1')
280
def new_callback(self, event):
281
dirname, basename = self.io.defaultfilename()
282
self.flist.new(dirname)
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
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])
295
line = self.text.get("insert linestart", "insert lineend")
296
for insertpt in xrange(len(line)):
297
if line[insertpt] not in (' ','\t'):
302
lineat = int(self.text.index("insert").split('.')[1])
304
if insertpt == lineat:
307
dest = "insert linestart+"+str(insertpt)+"c"
309
if (event.state&1) == 0:
311
self.text.tag_remove("sel", "1.0", "end")
313
if not self.text.index("sel.first"):
314
self.text.mark_set("anchor","insert")
316
first = self.text.index(dest)
317
last = self.text.index("anchor")
319
if self.text.compare(first,">",last):
320
first,last = last,first
322
self.text.tag_remove("sel", "1.0", "end")
323
self.text.tag_add("sel", first, last)
325
self.text.mark_set("insert", dest)
326
self.text.see("insert")
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)
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)
351
("format", "F_ormat"),
353
("options", "_Options"),
354
("windows", "_Windows"),
358
if macosxSupport.runningAsOSXApp():
360
menu_specs[-2] = ("windows", "_Window")
363
def createmenubar(self):
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)
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)
377
self.base_helpmenu_length = self.menudict['help'].index(END)
378
self.reset_help_menu_entries()
380
def postwindowsmenu(self):
381
# Only called when Windows menu exists
382
menu = self.menudict['windows']
383
end = menu.index("end")
386
if end > self.wmenu_end:
387
menu.delete(self.wmenu_end+1, end)
388
WindowList.add_windows_to_menu(menu)
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))
399
iswin = sys.platform[:3] == 'win'
401
self.text.config(cursor="arrow")
402
rmenu.tk_popup(event.x_root, event.y_root)
404
self.text.config(cursor="ibeam")
407
# ("Label", "<<virtual-event>>"), ...
408
("Close", "<<close-window>>"), # Example
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)
419
def about_dialog(self, event=None):
420
aboutDialog.AboutDialog(self.top,'About IDLE')
422
def config_dialog(self, event=None):
423
configDialog.ConfigDialog(self.top,'Settings')
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)
429
def python_docs(self, event=None):
430
if sys.platform[:3] == 'win':
431
os.startfile(self.help_url)
433
webbrowser.open(self.help_url)
437
self.text.event_generate("<<Cut>>")
440
def copy(self,event):
441
if not self.text.tag_ranges("sel"):
442
# There is no selection, so do nothing and maybe interrupt.
444
self.text.event_generate("<<Copy>>")
447
def paste(self,event):
448
self.text.event_generate("<<Paste>>")
449
self.text.see("insert")
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")
458
def remove_selection(self, event=None):
459
self.text.tag_remove("sel", "1.0", "end")
460
self.text.see("insert")
462
def move_at_edge_if_selection(self, edge_index):
463
"""Cursor move begins at start or end of selection
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
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
476
self_text_index("sel.first")
477
self_text_mark_set("insert", edges_table[edge_index])
482
def del_word_left(self, event):
483
self.text.event_generate('<Meta-Delete>')
486
def del_word_right(self, event):
487
self.text.event_generate('<Meta-d>')
490
def find_event(self, event):
491
SearchDialog.find(self.text)
494
def find_again_event(self, event):
495
SearchDialog.find_again(self.text)
498
def find_selection_event(self, event):
499
SearchDialog.find_selection(self.text)
502
def find_in_files_event(self, event):
503
GrepDialog.grep(self.text, self.io, self.flist)
506
def replace_event(self, event):
507
ReplaceDialog.replace(self.text)
510
def goto_line_event(self, event):
512
lineno = tkSimpleDialog.askinteger("Goto",
513
"Go to line number:",parent=text)
519
text.mark_set("insert", "%d.0" % lineno)
522
def open_module(self, event=None):
523
# XXX Shouldn't this be in IOBinding or in FileList?
525
name = self.text.get("sel.first", "sel.last")
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)
538
# XXX Ought to insert current file's directory in front of path
540
(f, file, (suffix, mode, type)) = _find_module(name)
541
except (NameError, ImportError), msg:
542
tkMessageBox.showerror("Import error", str(msg), parent=self.text)
544
if type != imp.PY_SOURCE:
545
tkMessageBox.showerror("Unsupported type",
546
"%s is not a source module" % name, parent=self.text)
551
self.flist.open(file)
553
self.io.loadfile(file)
555
def open_class_browser(self, event=None):
556
filename = self.io.filename
558
tkMessageBox.showerror(
560
"This buffer has no associated filename",
562
self.text.focus_set()
564
head, tail = os.path.split(filename)
565
base, ext = os.path.splitext(tail)
567
ClassBrowser.ClassBrowser(self.flist, base, [head])
569
def open_path_browser(self, event=None):
571
PathBrowser.PathBrowser(self.flist)
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")
580
def ispythonsource(self, filename):
581
if not filename or os.path.isdir(filename):
583
base, ext = os.path.splitext(os.path.basename(filename))
584
if os.path.normcase(ext) in (".py", ".pyw"):
592
return line.startswith('#!') and line.find('python') >= 0
594
def close_hook(self):
596
self.flist.unregister_maybe_terminate(self)
599
def set_close_hook(self, close_hook):
600
self.close_hook = close_hook
602
def filename_change_hook(self):
604
self.flist.filename_changed_edit(self)
605
self.saved_change_hook()
606
self.top.update_windowlist_registry(self)
607
self.ResetColorizer()
609
def _addcolorizer(self):
612
if self.ispythonsource(self.io.filename):
613
self.color = self.ColorDelegator()
614
# can add more colorizers here...
616
self.per.removefilter(self.undo)
617
self.per.insertfilter(self.color)
618
self.per.insertfilter(self.undo)
620
def _rmcolorizer(self):
623
self.color.removecolors()
624
self.per.removefilter(self.color)
627
def ResetColorizer(self):
628
"Update the colour theme"
629
# Called from self.filename_change_hook and from configDialog.py
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')
637
foreground=normal_colors['foreground'],
638
background=normal_colors['background'],
639
insertbackground=cursor_color,
640
selectforeground=select_colors['foreground'],
641
selectbackground=select_colors['background'],
645
"Update the text widgets' font if it is changed"
646
# Called from configDialog.py
648
if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
650
self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
651
idleConf.GetOption('main','EditorWindow','font-size'),
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)
663
for event, keylist in xkeydefs.items():
664
self.text.event_delete(event, *keylist)
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)
674
self.apply_bindings(xkeydefs)
675
#update menu accelerators
677
for menu in self.Bindings.menudefs:
678
menuEventDict[menu[0]] = {}
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')
689
itemName = menu.entrycget(index, 'label')
691
if menuEventDict.has_key(menubarItem):
692
if menuEventDict[menubarItem].has_key(itemName):
693
event = menuEventDict[menubarItem][itemName]
695
accel = get_accelerator(keydefs, event)
696
menu.entryconfig(index, accelerator=accel)
698
def set_notabs_indentwidth(self):
699
"Update the indentwidth if changed and not using tabs in this window"
700
# Called from configDialog.py
702
self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
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)
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
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)
730
webbrowser.open(helpfile)
731
return display_extra_help
733
def update_recent_files_list(self, new_file=None):
734
"Load and update the recent files list and menus"
736
if os.path.exists(self.recent_files_path):
737
rf_list_file = open(self.recent_files_path,'r')
739
rf_list = rf_list_file.readlines()
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
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')
757
rf_file.writelines(rf_list)
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,
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
778
def saved_change_hook(self):
779
short = self.short_title()
780
long = self.long_title()
782
title = short + " - " + long
789
icon = short or long or title
790
if not self.get_saved():
791
title = "*%s*" % title
793
self.top.wm_title(title)
794
self.top.wm_iconname(icon)
797
return self.undo.get_saved()
799
def set_saved(self, flag):
800
self.undo.set_saved(flag)
802
def reset_undo(self):
803
self.undo.reset_undo()
805
def short_title(self):
806
filename = self.io.filename
808
filename = os.path.basename(filename)
809
# return unicode string to display non-ASCII chars correctly
810
return self._filename_to_unicode(filename)
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 "")
816
def center_insert_event(self, event):
819
def center(self, mark="insert"):
821
top, bot = self.getwindowlines()
822
lineno = self.getlineno(mark)
824
newtop = max(1, lineno - height//2)
825
text.yview(float(newtop))
827
def getwindowlines(self):
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
837
def getlineno(self, mark="insert"):
839
return int(float(text.index(mark)))
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()))
848
def close_event(self, event):
853
if not self.get_saved():
854
if self.top.state()!='normal':
858
return self.io.maybesave()
861
reply = self.maybesave()
862
if str(reply) != "cancel":
868
self.update_recent_files_list(new_file=self.io.filename)
869
WindowList.unregister_callback(self.postwindowsmenu)
870
self.unload_extensions()
875
self.color.close(False)
878
self.tkinter_vars = None
883
# unless override: unregister from flist, terminate if last window
886
def load_extensions(self):
888
self.load_standard_extensions()
890
def unload_extensions(self):
891
for ins in self.extensions.values():
892
if hasattr(ins, "close"):
896
def load_standard_extensions(self):
897
for name in self.get_standard_extension_names():
899
self.load_extension(name)
901
print "Failed to load extension", repr(name)
903
traceback.print_exc()
905
def get_standard_extension_names(self):
906
return idleConf.GetExtensions(editor_only=True)
908
def load_extension(self, name):
910
mod = __import__(name, globals(), locals(), [])
912
print "\nFailed to import extension: ", name
914
cls = getattr(mod, name)
915
keydefs = idleConf.GetExtensionBindings(name)
916
if hasattr(cls, "menudefs"):
917
self.fill_menus(cls.menudefs, keydefs)
919
self.extensions[name] = ins
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))
932
def apply_bindings(self, keydefs=None):
934
keydefs = self.Bindings.default_keydefs
936
text.keydefs = keydefs
937
for event, keylist in keydefs.items():
939
text.event_add(event, *keylist)
941
def fill_menus(self, menudefs=None, keydefs=None):
942
"""Add appropriate entries to the menus and submenus
944
Menus that are absent or None in self.menudict are ignored.
947
menudefs = self.Bindings.menudefs
949
keydefs = self.Bindings.default_keydefs
950
menudict = self.menudict
952
for mname, entrylist in menudefs:
953
menu = menudict.get(mname)
956
for entry in entrylist:
960
label, eventname = entry
961
checkbutton = (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)
969
var = self.get_var_obj(eventname, BooleanVar)
970
menu.add_checkbutton(label=label, underline=underline,
971
command=command, accelerator=accelerator,
974
menu.add_command(label=label, underline=underline,
976
accelerator=accelerator)
978
def getvar(self, name):
979
var = self.get_var_obj(name)
984
raise NameError, name
986
def setvar(self, name, value, vartype=None):
987
var = self.get_var_obj(name, vartype)
991
raise NameError, name
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)
1000
# Tk implementations of "virtual text methods" -- each platform
1001
# reusing IDLE's support code needs to define these for its GUI's
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.
1009
def is_char_in_string(self, text_index):
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)
1016
# The colorizer is missing: assume the worst
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):
1023
first = self.text.index("sel.first")
1024
last = self.text.index("sel.last")
1029
# Return the text widget's current view of what a tab stop means
1030
# (equivalent width in spaces).
1032
def get_tabwidth(self):
1033
current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
1036
# Set the text widget's current view of what a tab stop means.
1038
def set_tabwidth(self, newtabwidth):
1040
if self.get_tabwidth() != newtabwidth:
1041
pixels = text.tk.call("font", "measure", text["font"],
1042
"-displayof", text.master,
1044
text.configure(tabs=pixels)
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
1052
def set_indentation_params(self, ispythonsource, guess=True):
1053
if guess and ispythonsource:
1054
i = self.guess_indent()
1056
self.indentwidth = i
1057
if self.indentwidth != self.tabwidth:
1058
self.usetabs = False
1059
self.set_tabwidth(self.tabwidth)
1061
def smart_backspace_event(self, event):
1063
first, last = self.get_selection_indices()
1065
text.delete(first, last)
1066
text.mark_set("insert", first)
1068
# Delete whitespace left, until hitting a real char or closest
1069
# preceding virtual tab stop.
1070
chars = text.get("insert linestart", "insert")
1072
if text.compare("insert", ">", "1.0"):
1073
# easy: delete preceding newline
1074
text.delete("insert-1c")
1076
text.bell() # at start of buffer
1078
if chars[-1] not in " \t":
1079
# easy: delete preceding real char
1080
text.delete("insert-1c")
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))
1087
want = ((have - 1) // self.indentwidth) * self.indentwidth
1088
# Debug prompt is multilined....
1089
last_line_of_prompt = sys.ps1.split('\n')[-1]
1092
if chars == last_line_of_prompt:
1095
ncharsdeleted = ncharsdeleted + 1
1096
have = len(chars.expandtabs(tabwidth))
1097
if have <= want or chars[-1] not in " \t":
1099
text.undo_block_start()
1100
text.delete("insert-%dc" % ncharsdeleted, "insert")
1102
text.insert("insert", ' ' * (want - have))
1103
text.undo_block_stop()
1106
def smart_indent_event(self, event):
1107
# if intraline selection:
1109
# elif multiline selection:
1114
first, last = self.get_selection_indices()
1115
text.undo_block_start()
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)
1128
# tab to the next 'stop' within or to right of line's text:
1132
effective = len(prefix.expandtabs(self.tabwidth))
1133
n = self.indentwidth
1134
pad = ' ' * (n - effective % n)
1135
text.insert("insert", pad)
1139
text.undo_block_stop()
1141
def newline_and_indent_event(self, event):
1143
first, last = self.get_selection_indices()
1144
text.undo_block_start()
1147
text.delete(first, last)
1148
text.mark_set("insert", first)
1149
line = text.get("insert linestart", "insert")
1151
while i < n and line[i] in " \t":
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')
1159
# strip whitespace before insert point unless it's in the prompt
1161
last_line_of_prompt = sys.ps1.split('\n')[-1]
1162
while line and line[-1] in " \t" and line != last_line_of_prompt:
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")
1171
text.insert("insert", '\n')
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")
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:
1190
r = text.tag_prevrange("console", "insert")
1194
startatindex = "1.0"
1195
rawtext = text.get(startatindex, "insert")
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
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
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)
1224
self.reindent_to(y.compute_backslash_indent())
1226
assert 0, "bogus continuation type %r" % (c,)
1229
# This line starts a brand new stmt; indent relative to
1230
# indentation of initial line of closest preceding
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)
1241
text.undo_block_stop()
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
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)
1254
def indent_region_event(self, event):
1255
head, tail, chars, lines = self.get_region()
1256
for pos in range(len(lines)):
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)
1265
def dedent_region_event(self, event):
1266
head, tail, chars, lines = self.get_region()
1267
for pos in range(len(lines)):
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)
1276
def comment_region_event(self, event):
1277
head, tail, chars, lines = self.get_region()
1278
for pos in range(len(lines) - 1):
1280
lines[pos] = '##' + line
1281
self.set_region(head, tail, chars, lines)
1283
def uncomment_region_event(self, event):
1284
head, tail, chars, lines = self.get_region()
1285
for pos in range(len(lines)):
1289
if line[:2] == '##':
1291
elif line[:1] == '#':
1294
self.set_region(head, tail, chars, lines)
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)):
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)
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)
1314
def toggle_tabs_event(self, event):
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",
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
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)
1336
def change_indentwidth_event(self, event):
1337
new = self.askinteger(
1339
"New indent width (2-16)\n(Always use 8 when using tabs)",
1341
initialvalue=self.indentwidth,
1344
if new and new != self.indentwidth and not self.usetabs:
1345
self.indentwidth = new
1348
def get_region(self):
1350
first, last = self.get_selection_indices()
1352
head = text.index(first + " linestart")
1353
tail = text.index(last + "-1c lineend +1c")
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
1361
def set_region(self, head, tail, chars, lines):
1363
newchars = "\n".join(lines)
1364
if newchars == chars:
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")
1375
# Make string that displays as n leading blanks.
1377
def _make_blanks(self, n):
1379
ntabs, nspaces = divmod(n, self.tabwidth)
1380
return '\t' * ntabs + ' ' * nspaces
1384
# Delete from beginning of line to insert point, then reinsert
1385
# column logical (meaning use tabs if appropriate) spaces.
1387
def reindent_to(self, column):
1389
text.undo_block_start()
1390
if text.compare("insert linestart", "!=", "insert"):
1391
text.delete("insert linestart", "insert")
1393
text.insert("insert", self._make_blanks(column))
1394
text.undo_block_stop()
1396
def _asktabwidth(self):
1397
return self.askinteger(
1399
"Columns per tab? (2-16)",
1401
initialvalue=self.indentwidth,
1403
maxvalue=16) or self.tabwidth
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).
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)
1416
indentsmall = indentlarge = 0
1417
return indentlarge - indentsmall
1419
# "line.col" -> line, as an int
1420
def index2line(index):
1421
return int(float(index))
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)
1428
def classifyws(s, tabwidth):
1433
effective = effective + 1
1436
effective = (effective // tabwidth + 1) * tabwidth
1439
return raw, effective
1442
_tokenize = tokenize
1445
class IndentSearcher(object):
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.
1452
def __init__(self, text, tabwidth):
1454
self.tabwidth = tabwidth
1455
self.i = self.finished = 0
1456
self.blkopenline = self.indentedline = None
1461
i = self.i = self.i + 1
1462
mark = repr(i) + ".0"
1463
if self.text.compare(mark, ">=", "end"):
1465
return self.text.get(mark, mark + " lineend+1c")
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')):
1473
elif type == NAME and token in OPENERS:
1474
self.blkopenline = line
1475
elif type == INDENT and self.blkopenline:
1476
self.indentedline = line
1480
save_tabsize = _tokenize.tabsize
1481
_tokenize.tabsize = self.tabwidth
1484
_tokenize.tokenize(self.readline, self.tokeneater)
1485
except _tokenize.TokenError:
1486
# since we cut off the tokenizer early, we can trigger
1490
_tokenize.tabsize = save_tabsize
1491
return self.blkopenline, self.indentedline
1493
### end autoindent code ###
1496
# Helper to extract the underscore from a string, e.g.
1497
# prepstr("Co_py") returns (2, "Copy").
1506
'bracketright': ']',
1510
def get_accelerator(keydefs, eventname):
1511
keylist = keydefs.get(eventname)
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)
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)
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_]')
1541
filename = sys.argv[1]
1544
edit = EditorWindow(root=root, filename=filename)
1545
edit.set_close_hook(root.quit)
1546
edit.text.bind("<<close-all-windows>>", edit.close_event)
1550
if __name__ == '__main__':