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