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