Update for release.
[python/dscho.git] / Tools / idle / EditorWindow.py
blobe8c310a1cc1fa7585bb4d66454c4ed85ccce6be3
1 import sys
2 import os
3 import re
4 import imp
5 from Tkinter import *
6 import tkSimpleDialog
7 import tkMessageBox
9 import webbrowser
10 import idlever
11 import WindowList
12 from IdleConf import idleconf
14 # The default tab setting for a Text widget, in average-width characters.
15 TK_TABWIDTH_DEFAULT = 8
17 # File menu
19 #$ event <<open-module>>
20 #$ win <Alt-m>
21 #$ unix <Control-x><Control-m>
23 #$ event <<open-class-browser>>
24 #$ win <Alt-c>
25 #$ unix <Control-x><Control-b>
27 #$ event <<open-path-browser>>
29 #$ event <<close-window>>
31 #$ unix <Control-x><Control-0>
32 #$ unix <Control-x><Key-0>
33 #$ win <Alt-F4>
35 # Edit menu
37 #$ event <<Copy>>
38 #$ win <Control-c>
39 #$ unix <Alt-w>
41 #$ event <<Cut>>
42 #$ win <Control-x>
43 #$ unix <Control-w>
45 #$ event <<Paste>>
46 #$ win <Control-v>
47 #$ unix <Control-y>
49 #$ event <<select-all>>
50 #$ win <Alt-a>
51 #$ unix <Alt-a>
53 # Help menu
55 #$ event <<help>>
56 #$ win <F1>
57 #$ unix <F1>
59 #$ event <<about-idle>>
61 # Events without menu entries
63 #$ event <<remove-selection>>
64 #$ win <Escape>
66 #$ event <<center-insert>>
67 #$ win <Control-l>
68 #$ unix <Control-l>
70 #$ event <<do-nothing>>
71 #$ unix <Control-x>
74 about_title = "About IDLE"
75 about_text = """\
76 IDLE %s
78 An Integrated DeveLopment Environment for Python
80 by Guido van Rossum
81 """ % idlever.IDLE_VERSION
83 def _find_module(fullname, path=None):
84 """Version of imp.find_module() that handles hierarchical module names"""
86 file = None
87 for tgt in fullname.split('.'):
88 if file is not None:
89 file.close() # close intermediate files
90 (file, filename, descr) = imp.find_module(tgt, path)
91 if descr[2] == imp.PY_SOURCE:
92 break # find but not load the source file
93 module = imp.load_module(tgt, file, filename, descr)
94 try:
95 path = module.__path__
96 except AttributeError:
97 raise ImportError, 'No source for module ' + module.__name__
98 return file, filename, descr
100 class EditorWindow:
102 from Percolator import Percolator
103 from ColorDelegator import ColorDelegator
104 from UndoDelegator import UndoDelegator
105 from IOBinding import IOBinding
106 import Bindings
107 from Tkinter import Toplevel
108 from MultiStatusBar import MultiStatusBar
110 about_title = about_title
111 about_text = about_text
113 vars = {}
114 runnable = False # Shell window cannot Import Module or Run Script
116 def __init__(self, flist=None, filename=None, key=None, root=None):
117 edconf = idleconf.getsection('EditorWindow')
118 coconf = idleconf.getsection('Colors')
119 self.flist = flist
120 root = root or flist.root
121 self.root = root
122 if flist:
123 self.vars = flist.vars
124 self.menubar = Menu(root)
125 self.top = top = self.Toplevel(root, menu=self.menubar)
126 self.vbar = vbar = Scrollbar(top, name='vbar')
127 self.text_frame = text_frame = Frame(top)
128 self.text = text = Text(text_frame, name='text', padx=5,
129 foreground=coconf.getdef('normal-foreground'),
130 background=coconf.getdef('normal-background'),
131 highlightcolor=coconf.getdef('hilite-foreground'),
132 highlightbackground=coconf.getdef('hilite-background'),
133 insertbackground=coconf.getdef('cursor-background'),
134 width=edconf.getint('width'),
135 height=edconf.getint('height'),
136 wrap="none")
138 self.createmenubar()
139 self.apply_bindings()
141 self.top.protocol("WM_DELETE_WINDOW", self.close)
142 self.top.bind("<<close-window>>", self.close_event)
143 text.bind("<<center-insert>>", self.center_insert_event)
144 text.bind("<<help>>", self.help_dialog)
145 text.bind("<<python-docs>>", self.python_docs)
146 text.bind("<<about-idle>>", self.about_dialog)
147 text.bind("<<open-module>>", self.open_module)
148 text.bind("<<do-nothing>>", lambda event: "break")
149 text.bind("<<select-all>>", self.select_all)
150 text.bind("<<remove-selection>>", self.remove_selection)
151 text.bind("<3>", self.right_menu_event)
152 if flist:
153 flist.inversedict[self] = key
154 if key:
155 flist.dict[key] = self
156 text.bind("<<open-new-window>>", self.flist.new_callback)
157 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
158 text.bind("<<open-class-browser>>", self.open_class_browser)
159 text.bind("<<open-path-browser>>", self.open_path_browser)
161 vbar['command'] = text.yview
162 vbar.pack(side=RIGHT, fill=Y)
164 text['yscrollcommand'] = vbar.set
165 text['font'] = edconf.get('font-name'), edconf.get('font-size')
166 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
167 text.pack(side=TOP, fill=BOTH, expand=1)
168 text.focus_set()
170 self.per = per = self.Percolator(text)
171 if self.ispythonsource(filename):
172 self.color = color = self.ColorDelegator(); per.insertfilter(color)
173 ##print "Initial colorizer"
174 else:
175 ##print "No initial colorizer"
176 self.color = None
177 self.undo = undo = self.UndoDelegator(); per.insertfilter(undo)
178 self.io = io = self.IOBinding(self)
180 text.undo_block_start = undo.undo_block_start
181 text.undo_block_stop = undo.undo_block_stop
182 undo.set_saved_change_hook(self.saved_change_hook)
183 io.set_filename_change_hook(self.filename_change_hook)
185 if filename:
186 if os.path.exists(filename):
187 io.loadfile(filename)
188 else:
189 io.set_filename(filename)
191 self.saved_change_hook()
193 self.load_extensions()
195 menu = self.menudict.get('windows')
196 if menu:
197 end = menu.index("end")
198 if end is None:
199 end = -1
200 if end >= 0:
201 menu.add_separator()
202 end = end + 1
203 self.wmenu_end = end
204 WindowList.register_callback(self.postwindowsmenu)
206 # Some abstractions so IDLE extensions are cross-IDE
207 self.askyesno = tkMessageBox.askyesno
208 self.askinteger = tkSimpleDialog.askinteger
209 self.showerror = tkMessageBox.showerror
211 if self.extensions.has_key('AutoIndent'):
212 self.extensions['AutoIndent'].set_indentation_params(
213 self.ispythonsource(filename))
214 self.set_status_bar()
216 def set_status_bar(self):
217 self.status_bar = self.MultiStatusBar(self.text_frame)
218 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
219 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
220 self.status_bar.pack(side=BOTTOM, fill=X)
221 self.text.bind('<KeyRelease>', self.set_line_and_column)
222 self.text.bind('<ButtonRelease>', self.set_line_and_column)
223 self.text.after_idle(self.set_line_and_column)
225 def set_line_and_column(self, event=None):
226 line, column = self.text.index(INSERT).split('.')
227 self.status_bar.set_label('column', 'Col: %s' % column)
228 self.status_bar.set_label('line', 'Ln: %s' % line)
230 def wakeup(self):
231 if self.top.wm_state() == "iconic":
232 self.top.wm_deiconify()
233 else:
234 self.top.tkraise()
235 self.text.focus_set()
237 menu_specs = [
238 ("file", "_File"),
239 ("edit", "_Edit"),
240 ("windows", "_Windows"),
241 ("help", "_Help"),
244 def createmenubar(self):
245 mbar = self.menubar
246 self.menudict = menudict = {}
247 for name, label in self.menu_specs:
248 underline, label = prepstr(label)
249 menudict[name] = menu = Menu(mbar, name=name)
250 mbar.add_cascade(label=label, menu=menu, underline=underline)
251 self.fill_menus()
253 def postwindowsmenu(self):
254 # Only called when Windows menu exists
255 # XXX Actually, this Just-In-Time updating interferes badly
256 # XXX with the tear-off feature. It would be better to update
257 # XXX all Windows menus whenever the list of windows changes.
258 menu = self.menudict['windows']
259 end = menu.index("end")
260 if end is None:
261 end = -1
262 if end > self.wmenu_end:
263 menu.delete(self.wmenu_end+1, end)
264 WindowList.add_windows_to_menu(menu)
266 rmenu = None
268 def right_menu_event(self, event):
269 self.text.tag_remove("sel", "1.0", "end")
270 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
271 if not self.rmenu:
272 self.make_rmenu()
273 rmenu = self.rmenu
274 self.event = event
275 iswin = sys.platform[:3] == 'win'
276 if iswin:
277 self.text.config(cursor="arrow")
278 rmenu.tk_popup(event.x_root, event.y_root)
279 if iswin:
280 self.text.config(cursor="ibeam")
282 rmenu_specs = [
283 # ("Label", "<<virtual-event>>"), ...
284 ("Close", "<<close-window>>"), # Example
287 def make_rmenu(self):
288 rmenu = Menu(self.text, tearoff=0)
289 for label, eventname in self.rmenu_specs:
290 def command(text=self.text, eventname=eventname):
291 text.event_generate(eventname)
292 rmenu.add_command(label=label, command=command)
293 self.rmenu = rmenu
295 def about_dialog(self, event=None):
296 tkMessageBox.showinfo(self.about_title, self.about_text,
297 master=self.text)
299 helpfile = "help.txt"
301 def help_dialog(self, event=None):
302 try:
303 helpfile = os.path.join(os.path.dirname(__file__), self.helpfile)
304 except NameError:
305 helpfile = self.helpfile
306 if self.flist:
307 self.flist.open(helpfile)
308 else:
309 self.io.loadfile(helpfile)
311 help_url = "http://www.python.org/doc/current/"
312 if sys.platform[:3] == "win":
313 fn = os.path.dirname(__file__)
314 fn = os.path.join(fn, os.pardir, os.pardir, "pythlp.chm")
315 fn = os.path.normpath(fn)
316 if os.path.isfile(fn):
317 help_url = fn
318 else:
319 fn = os.path.dirname(__file__)
320 fn = os.path.join(fn, os.pardir, os.pardir, "Doc", "index.html")
321 fn = os.path.normpath(fn)
322 if os.path.isfile(fn):
323 help_url = fn
324 del fn
326 def python_docs(self, event=None):
327 os.startfile(self.help_url)
328 else:
329 def python_docs(self, event=None):
330 webbrowser.open(self.help_url)
332 def select_all(self, event=None):
333 self.text.tag_add("sel", "1.0", "end-1c")
334 self.text.mark_set("insert", "1.0")
335 self.text.see("insert")
336 return "break"
338 def remove_selection(self, event=None):
339 self.text.tag_remove("sel", "1.0", "end")
340 self.text.see("insert")
342 def open_module(self, event=None):
343 # XXX Shouldn't this be in IOBinding or in FileList?
344 try:
345 name = self.text.get("sel.first", "sel.last")
346 except TclError:
347 name = ""
348 else:
349 name = name.strip()
350 if not name:
351 name = tkSimpleDialog.askstring("Module",
352 "Enter the name of a Python module\n"
353 "to search on sys.path and open:",
354 parent=self.text)
355 if name:
356 name = name.strip()
357 if not name:
358 return
359 # XXX Ought to insert current file's directory in front of path
360 try:
361 (f, file, (suffix, mode, type)) = _find_module(name)
362 except (NameError, ImportError), msg:
363 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
364 return
365 if type != imp.PY_SOURCE:
366 tkMessageBox.showerror("Unsupported type",
367 "%s is not a source module" % name, parent=self.text)
368 return
369 if f:
370 f.close()
371 if self.flist:
372 self.flist.open(file)
373 else:
374 self.io.loadfile(file)
376 def open_class_browser(self, event=None):
377 filename = self.io.filename
378 if not filename:
379 tkMessageBox.showerror(
380 "No filename",
381 "This buffer has no associated filename",
382 master=self.text)
383 self.text.focus_set()
384 return None
385 head, tail = os.path.split(filename)
386 base, ext = os.path.splitext(tail)
387 import ClassBrowser
388 ClassBrowser.ClassBrowser(self.flist, base, [head])
390 def open_path_browser(self, event=None):
391 import PathBrowser
392 PathBrowser.PathBrowser(self.flist)
394 def gotoline(self, lineno):
395 if lineno is not None and lineno > 0:
396 self.text.mark_set("insert", "%d.0" % lineno)
397 self.text.tag_remove("sel", "1.0", "end")
398 self.text.tag_add("sel", "insert", "insert +1l")
399 self.center()
401 def ispythonsource(self, filename):
402 if not filename:
403 return True
404 base, ext = os.path.splitext(os.path.basename(filename))
405 if os.path.normcase(ext) in (".py", ".pyw"):
406 return True
407 try:
408 f = open(filename)
409 line = f.readline()
410 f.close()
411 except IOError:
412 return False
413 return line.startswith('#!') and 'python' in line
415 def close_hook(self):
416 if self.flist:
417 self.flist.close_edit(self)
419 def set_close_hook(self, close_hook):
420 self.close_hook = close_hook
422 def filename_change_hook(self):
423 if self.flist:
424 self.flist.filename_changed_edit(self)
425 self.saved_change_hook()
426 if self.ispythonsource(self.io.filename):
427 self.addcolorizer()
428 else:
429 self.rmcolorizer()
431 def addcolorizer(self):
432 if self.color:
433 return
434 ##print "Add colorizer"
435 self.per.removefilter(self.undo)
436 self.color = self.ColorDelegator()
437 self.per.insertfilter(self.color)
438 self.per.insertfilter(self.undo)
440 def rmcolorizer(self):
441 if not self.color:
442 return
443 ##print "Remove colorizer"
444 self.per.removefilter(self.undo)
445 self.per.removefilter(self.color)
446 self.color = None
447 self.per.insertfilter(self.undo)
449 def saved_change_hook(self):
450 short = self.short_title()
451 long = self.long_title()
452 if short and long:
453 title = short + " - " + long
454 elif short:
455 title = short
456 elif long:
457 title = long
458 else:
459 title = "Untitled"
460 icon = short or long or title
461 if not self.get_saved():
462 title = "*%s*" % title
463 icon = "*%s" % icon
464 self.top.wm_title(title)
465 self.top.wm_iconname(icon)
467 def get_saved(self):
468 return self.undo.get_saved()
470 def set_saved(self, flag):
471 self.undo.set_saved(flag)
473 def reset_undo(self):
474 self.undo.reset_undo()
476 def short_title(self):
477 filename = self.io.filename
478 if filename:
479 filename = os.path.basename(filename)
480 return filename
482 def long_title(self):
483 return self.io.filename or ""
485 def center_insert_event(self, event):
486 self.center()
488 def center(self, mark="insert"):
489 text = self.text
490 top, bot = self.getwindowlines()
491 lineno = self.getlineno(mark)
492 height = bot - top
493 newtop = max(1, lineno - height//2)
494 text.yview(float(newtop))
496 def getwindowlines(self):
497 text = self.text
498 top = self.getlineno("@0,0")
499 bot = self.getlineno("@0,65535")
500 if top == bot and text.winfo_height() == 1:
501 # Geometry manager hasn't run yet
502 height = int(text['height'])
503 bot = top + height - 1
504 return top, bot
506 def getlineno(self, mark="insert"):
507 text = self.text
508 return int(float(text.index(mark)))
510 def close_event(self, event):
511 self.close()
513 def maybesave(self):
514 if self.io:
515 return self.io.maybesave()
517 def close(self):
518 self.top.wm_deiconify()
519 self.top.tkraise()
520 reply = self.maybesave()
521 if reply != "cancel":
522 self._close()
523 return reply
525 def _close(self):
526 WindowList.unregister_callback(self.postwindowsmenu)
527 if self.close_hook:
528 self.close_hook()
529 self.flist = None
530 colorizing = 0
531 self.unload_extensions()
532 self.io.close(); self.io = None
533 self.undo = None # XXX
534 if self.color:
535 colorizing = self.color.colorizing
536 doh = colorizing and self.top
537 self.color.close(doh) # Cancel colorization
538 self.text = None
539 self.vars = None
540 self.per.close(); self.per = None
541 if not colorizing:
542 self.top.destroy()
544 def load_extensions(self):
545 self.extensions = {}
546 self.load_standard_extensions()
548 def unload_extensions(self):
549 for ins in self.extensions.values():
550 if hasattr(ins, "close"):
551 ins.close()
552 self.extensions = {}
554 def load_standard_extensions(self):
555 for name in self.get_standard_extension_names():
556 try:
557 self.load_extension(name)
558 except:
559 print "Failed to load extension", `name`
560 import traceback
561 traceback.print_exc()
563 def get_standard_extension_names(self):
564 return idleconf.getextensions()
566 def load_extension(self, name):
567 mod = __import__(name, globals(), locals(), [])
568 cls = getattr(mod, name)
569 ins = cls(self)
570 self.extensions[name] = ins
571 kdnames = ["keydefs"]
572 if sys.platform == 'win32':
573 kdnames.append("windows_keydefs")
574 elif sys.platform == 'mac':
575 kdnames.append("mac_keydefs")
576 else:
577 kdnames.append("unix_keydefs")
578 keydefs = {}
579 for kdname in kdnames:
580 if hasattr(ins, kdname):
581 keydefs.update(getattr(ins, kdname))
582 if keydefs:
583 self.apply_bindings(keydefs)
584 for vevent in keydefs.keys():
585 methodname = vevent.replace("-", "_")
586 while methodname[:1] == '<':
587 methodname = methodname[1:]
588 while methodname[-1:] == '>':
589 methodname = methodname[:-1]
590 methodname = methodname + "_event"
591 if hasattr(ins, methodname):
592 self.text.bind(vevent, getattr(ins, methodname))
593 if hasattr(ins, "menudefs"):
594 self.fill_menus(ins.menudefs, keydefs)
595 return ins
597 def apply_bindings(self, keydefs=None):
598 if keydefs is None:
599 keydefs = self.Bindings.default_keydefs
600 text = self.text
601 text.keydefs = keydefs
602 for event, keylist in keydefs.items():
603 if keylist:
604 apply(text.event_add, (event,) + tuple(keylist))
606 def fill_menus(self, defs=None, keydefs=None):
607 # Fill the menus. Menus that are absent or None in
608 # self.menudict are ignored.
609 if defs is None:
610 defs = self.Bindings.menudefs
611 if keydefs is None:
612 keydefs = self.Bindings.default_keydefs
613 menudict = self.menudict
614 text = self.text
615 for mname, itemlist in defs:
616 menu = menudict.get(mname)
617 if not menu:
618 continue
619 for item in itemlist:
620 if not item:
621 menu.add_separator()
622 else:
623 label, event = item
624 checkbutton = (label[:1] == '!')
625 if checkbutton:
626 label = label[1:]
627 underline, label = prepstr(label)
628 accelerator = get_accelerator(keydefs, event)
629 def command(text=text, event=event):
630 text.event_generate(event)
631 if checkbutton:
632 var = self.getrawvar(event, BooleanVar)
633 menu.add_checkbutton(label=label, underline=underline,
634 command=command, accelerator=accelerator,
635 variable=var)
636 else:
637 menu.add_command(label=label, underline=underline,
638 command=command, accelerator=accelerator)
640 def getvar(self, name):
641 var = self.getrawvar(name)
642 if var:
643 return var.get()
645 def setvar(self, name, value, vartype=None):
646 var = self.getrawvar(name, vartype)
647 if var:
648 var.set(value)
650 def getrawvar(self, name, vartype=None):
651 var = self.vars.get(name)
652 if not var and vartype:
653 self.vars[name] = var = vartype(self.text)
654 return var
656 # Tk implementations of "virtual text methods" -- each platform
657 # reusing IDLE's support code needs to define these for its GUI's
658 # flavor of widget.
660 # Is character at text_index in a Python string? Return 0 for
661 # "guaranteed no", true for anything else. This info is expensive
662 # to compute ab initio, but is probably already known by the
663 # platform's colorizer.
665 def is_char_in_string(self, text_index):
666 if self.color:
667 # Return true iff colorizer hasn't (re)gotten this far
668 # yet, or the character is tagged as being in a string
669 return self.text.tag_prevrange("TODO", text_index) or \
670 "STRING" in self.text.tag_names(text_index)
671 else:
672 # The colorizer is missing: assume the worst
673 return 1
675 # If a selection is defined in the text widget, return (start,
676 # end) as Tkinter text indices, otherwise return (None, None)
677 def get_selection_indices(self):
678 try:
679 first = self.text.index("sel.first")
680 last = self.text.index("sel.last")
681 return first, last
682 except TclError:
683 return None, None
685 # Return the text widget's current view of what a tab stop means
686 # (equivalent width in spaces).
688 def get_tabwidth(self):
689 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
690 return int(current)
692 # Set the text widget's current view of what a tab stop means.
694 def set_tabwidth(self, newtabwidth):
695 text = self.text
696 if self.get_tabwidth() != newtabwidth:
697 pixels = text.tk.call("font", "measure", text["font"],
698 "-displayof", text.master,
699 "n" * newtabwidth)
700 text.configure(tabs=pixels)
702 def prepstr(s):
703 # Helper to extract the underscore from a string, e.g.
704 # prepstr("Co_py") returns (2, "Copy").
705 i = s.find('_')
706 if i >= 0:
707 s = s[:i] + s[i+1:]
708 return i, s
711 keynames = {
712 'bracketleft': '[',
713 'bracketright': ']',
714 'slash': '/',
717 def get_accelerator(keydefs, event):
718 keylist = keydefs.get(event)
719 if not keylist:
720 return ""
721 s = keylist[0]
722 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
723 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
724 s = re.sub("Key-", "", s)
725 s = re.sub("Control-", "Ctrl-", s)
726 s = re.sub("-", "+", s)
727 s = re.sub("><", " ", s)
728 s = re.sub("<", "", s)
729 s = re.sub(">", "", s)
730 return s
733 def fixwordbreaks(root):
734 # Make sure that Tk's double-click and next/previous word
735 # operations use our definition of a word (i.e. an identifier)
736 tk = root.tk
737 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
738 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
739 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
742 def test():
743 root = Tk()
744 fixwordbreaks(root)
745 root.withdraw()
746 if sys.argv[1:]:
747 filename = sys.argv[1]
748 else:
749 filename = None
750 edit = EditorWindow(root=root, filename=filename)
751 edit.set_close_hook(root.quit)
752 root.mainloop()
753 root.destroy()
755 if __name__ == '__main__':
756 test()