Get started on 2.2a2 NEWS.
[python/dscho.git] / Lib / idlelib / EditorWindow.py
blob3f3f95b6b3df3cf21240357af066ee1a1c67320b
1 # changes by dscherer@cmu.edu
2 # - created format and run menus
3 # - added silly advice dialog (apologies to Douglas Adams)
4 # - made Python Documentation work on Windows (requires win32api to
5 # do a ShellExecute(); other ways of starting a web browser are awkward)
7 import sys
8 import os
9 import string
10 import re
11 import imp
12 from Tkinter import *
13 import tkSimpleDialog
14 import tkMessageBox
16 import webbrowser
17 import idlever
18 import WindowList
19 from IdleConf import idleconf
21 # The default tab setting for a Text widget, in average-width characters.
22 TK_TABWIDTH_DEFAULT = 8
24 # File menu
26 #$ event <<open-module>>
27 #$ win <Alt-m>
28 #$ unix <Control-x><Control-m>
30 #$ event <<open-class-browser>>
31 #$ win <Alt-c>
32 #$ unix <Control-x><Control-b>
34 #$ event <<open-path-browser>>
36 #$ event <<close-window>>
38 #$ unix <Control-x><Control-0>
39 #$ unix <Control-x><Key-0>
40 #$ win <Alt-F4>
42 # Edit menu
44 #$ event <<Copy>>
45 #$ win <Control-c>
46 #$ unix <Alt-w>
48 #$ event <<Cut>>
49 #$ win <Control-x>
50 #$ unix <Control-w>
52 #$ event <<Paste>>
53 #$ win <Control-v>
54 #$ unix <Control-y>
56 #$ event <<select-all>>
57 #$ win <Alt-a>
58 #$ unix <Alt-a>
60 # Help menu
62 #$ event <<help>>
63 #$ win <F1>
64 #$ unix <F1>
66 #$ event <<about-idle>>
68 # Events without menu entries
70 #$ event <<remove-selection>>
71 #$ win <Escape>
73 #$ event <<center-insert>>
74 #$ win <Control-l>
75 #$ unix <Control-l>
77 #$ event <<do-nothing>>
78 #$ unix <Control-x>
81 about_title = "About IDLEfork"
82 about_text = """\
83 IDLEfork %s
85 IDLE is an Integrated DeveLopment Environment for Python \
86 by Guido van Rossum.
88 IDLEfork is an official experimental development version of IDLE.
89 Succesful new features in IDLEfork will be mereged back in to stable IDLE.
91 This version of IDLEfork is based on the work in stable IDLE version %s, \
92 IDLEfork 0.7.1 released by David Scherer, and the VPython idle fork.
94 See README.txt for more details on IDLEfork.
96 WARNING: IDLEfork is at this stage alpha quality software, expect things \
97 to be broken.
99 """ % (idlever.IDLEFORK_VERSION, idlever.IDLE_VERSION)
101 class EditorWindow:
103 from Percolator import Percolator
104 from ColorDelegator import ColorDelegator
105 from UndoDelegator import UndoDelegator
106 from IOBinding import IOBinding
107 import Bindings
108 from Tkinter import Toplevel
109 from MultiStatusBar import MultiStatusBar
111 about_title = about_title
112 about_text = about_text
114 vars = {}
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("<<good-advice>>", self.good_advice)
146 text.bind("<<python-docs>>", self.python_docs)
147 text.bind("<<about-idle>>", self.about_dialog)
148 text.bind("<<open-module>>", self.open_module)
149 text.bind("<<do-nothing>>", lambda event: "break")
150 text.bind("<<select-all>>", self.select_all)
151 text.bind("<<remove-selection>>", self.remove_selection)
152 text.bind("<3>", self.right_menu_event)
153 if flist:
154 flist.inversedict[self] = key
155 if key:
156 flist.dict[key] = self
157 text.bind("<<open-new-window>>", self.flist.new_callback)
158 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
159 text.bind("<<open-class-browser>>", self.open_class_browser)
160 text.bind("<<open-path-browser>>", self.open_path_browser)
162 vbar['command'] = text.yview
163 vbar.pack(side=RIGHT, fill=Y)
165 text['yscrollcommand'] = vbar.set
166 text['font'] = edconf.get('font-name'), edconf.get('font-size')
167 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
168 text.pack(side=TOP, fill=BOTH, expand=1)
169 text.focus_set()
171 self.per = per = self.Percolator(text)
172 if self.ispythonsource(filename):
173 self.color = color = self.ColorDelegator(); per.insertfilter(color)
174 ##print "Initial colorizer"
175 else:
176 ##print "No initial colorizer"
177 self.color = None
178 self.undo = undo = self.UndoDelegator(); per.insertfilter(undo)
179 self.io = io = self.IOBinding(self)
181 text.undo_block_start = undo.undo_block_start
182 text.undo_block_stop = undo.undo_block_stop
183 undo.set_saved_change_hook(self.saved_change_hook)
184 io.set_filename_change_hook(self.filename_change_hook)
186 if filename:
187 if os.path.exists(filename):
188 io.loadfile(filename)
189 else:
190 io.set_filename(filename)
192 self.saved_change_hook()
194 self.load_extensions()
196 menu = self.menudict.get('windows')
197 if menu:
198 end = menu.index("end")
199 if end is None:
200 end = -1
201 if end >= 0:
202 menu.add_separator()
203 end = end + 1
204 self.wmenu_end = end
205 WindowList.register_callback(self.postwindowsmenu)
207 # Some abstractions so IDLE extensions are cross-IDE
208 self.askyesno = tkMessageBox.askyesno
209 self.askinteger = tkSimpleDialog.askinteger
210 self.showerror = tkMessageBox.showerror
212 if self.extensions.has_key('AutoIndent'):
213 self.extensions['AutoIndent'].set_indentation_params(
214 self.ispythonsource(filename))
215 self.set_status_bar()
217 def set_status_bar(self):
218 self.status_bar = self.MultiStatusBar(self.text_frame)
219 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
220 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
221 self.status_bar.pack(side=BOTTOM, fill=X)
222 self.text.bind('<KeyRelease>', self.set_line_and_column)
223 self.text.bind('<ButtonRelease>', self.set_line_and_column)
224 self.text.after_idle(self.set_line_and_column)
226 def set_line_and_column(self, event=None):
227 line, column = string.split(self.text.index(INSERT), '.')
228 self.status_bar.set_label('column', 'Col: %s' % column)
229 self.status_bar.set_label('line', 'Ln: %s' % line)
231 def wakeup(self):
232 if self.top.wm_state() == "iconic":
233 self.top.wm_deiconify()
234 else:
235 self.top.tkraise()
236 self.text.focus_set()
238 menu_specs = [
239 ("file", "_File"),
240 ("edit", "_Edit"),
241 ("format", "F_ormat"),
242 ("run", "_Run"),
243 ("windows", "_Windows"),
244 ("help", "_Help"),
247 def createmenubar(self):
248 mbar = self.menubar
249 self.menudict = menudict = {}
250 for name, label in self.menu_specs:
251 underline, label = prepstr(label)
252 menudict[name] = menu = Menu(mbar, name=name)
253 mbar.add_cascade(label=label, menu=menu, underline=underline)
254 self.fill_menus()
256 def postwindowsmenu(self):
257 # Only called when Windows menu exists
258 # XXX Actually, this Just-In-Time updating interferes badly
259 # XXX with the tear-off feature. It would be better to update
260 # XXX all Windows menus whenever the list of windows changes.
261 menu = self.menudict['windows']
262 end = menu.index("end")
263 if end is None:
264 end = -1
265 if end > self.wmenu_end:
266 menu.delete(self.wmenu_end+1, end)
267 WindowList.add_windows_to_menu(menu)
269 rmenu = None
271 def right_menu_event(self, event):
272 self.text.tag_remove("sel", "1.0", "end")
273 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
274 if not self.rmenu:
275 self.make_rmenu()
276 rmenu = self.rmenu
277 self.event = event
278 iswin = sys.platform[:3] == 'win'
279 if iswin:
280 self.text.config(cursor="arrow")
281 rmenu.tk_popup(event.x_root, event.y_root)
282 if iswin:
283 self.text.config(cursor="ibeam")
285 rmenu_specs = [
286 # ("Label", "<<virtual-event>>"), ...
287 ("Close", "<<close-window>>"), # Example
290 def make_rmenu(self):
291 rmenu = Menu(self.text, tearoff=0)
292 for label, eventname in self.rmenu_specs:
293 def command(text=self.text, eventname=eventname):
294 text.event_generate(eventname)
295 rmenu.add_command(label=label, command=command)
296 self.rmenu = rmenu
298 def about_dialog(self, event=None):
299 tkMessageBox.showinfo(self.about_title, self.about_text,
300 master=self.text)
302 helpfile = "help.txt"
304 def good_advice(self, event=None):
305 tkMessageBox.showinfo('Advice', "Don't Panic!", master=self.text)
307 def help_dialog(self, event=None):
308 try:
309 helpfile = os.path.join(os.path.dirname(__file__), self.helpfile)
310 except NameError:
311 helpfile = self.helpfile
312 if self.flist:
313 self.flist.open(helpfile)
314 else:
315 self.io.loadfile(helpfile)
317 help_url = "http://www.python.org/doc/current/"
318 if sys.platform[:3] == "win":
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 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 = string.strip(name)
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 = string.strip(name)
354 if not name:
355 return
356 # XXX Ought to support package syntax
357 # XXX Ought to insert current file's directory in front of path
358 try:
359 (f, file, (suffix, mode, type)) = imp.find_module(name)
360 except (NameError, ImportError), msg:
361 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
362 return
363 if type != imp.PY_SOURCE:
364 tkMessageBox.showerror("Unsupported type",
365 "%s is not a source module" % name, parent=self.text)
366 return
367 if f:
368 f.close()
369 if self.flist:
370 self.flist.open(file)
371 else:
372 self.io.loadfile(file)
374 def open_class_browser(self, event=None):
375 filename = self.io.filename
376 if not filename:
377 tkMessageBox.showerror(
378 "No filename",
379 "This buffer has no associated filename",
380 master=self.text)
381 self.text.focus_set()
382 return None
383 head, tail = os.path.split(filename)
384 base, ext = os.path.splitext(tail)
385 import ClassBrowser
386 ClassBrowser.ClassBrowser(self.flist, base, [head])
388 def open_path_browser(self, event=None):
389 import PathBrowser
390 PathBrowser.PathBrowser(self.flist)
392 def gotoline(self, lineno):
393 if lineno is not None and lineno > 0:
394 self.text.mark_set("insert", "%d.0" % lineno)
395 self.text.tag_remove("sel", "1.0", "end")
396 self.text.tag_add("sel", "insert", "insert +1l")
397 self.center()
399 def ispythonsource(self, filename):
400 if not filename:
401 return 1
402 base, ext = os.path.splitext(os.path.basename(filename))
403 if os.path.normcase(ext) in (".py", ".pyw"):
404 return 1
405 try:
406 f = open(filename)
407 line = f.readline()
408 f.close()
409 except IOError:
410 return 0
411 return line[:2] == '#!' and string.find(line, 'python') >= 0
413 def close_hook(self):
414 if self.flist:
415 self.flist.close_edit(self)
417 def set_close_hook(self, close_hook):
418 self.close_hook = close_hook
420 def filename_change_hook(self):
421 if self.flist:
422 self.flist.filename_changed_edit(self)
423 self.saved_change_hook()
424 if self.ispythonsource(self.io.filename):
425 self.addcolorizer()
426 else:
427 self.rmcolorizer()
429 def addcolorizer(self):
430 if self.color:
431 return
432 ##print "Add colorizer"
433 self.per.removefilter(self.undo)
434 self.color = self.ColorDelegator()
435 self.per.insertfilter(self.color)
436 self.per.insertfilter(self.undo)
438 def rmcolorizer(self):
439 if not self.color:
440 return
441 ##print "Remove colorizer"
442 self.per.removefilter(self.undo)
443 self.per.removefilter(self.color)
444 self.color = None
445 self.per.insertfilter(self.undo)
447 def saved_change_hook(self):
448 short = self.short_title()
449 long = self.long_title()
450 if short and long:
451 title = short + " - " + long
452 elif short:
453 title = short
454 elif long:
455 title = long
456 else:
457 title = "Untitled"
458 icon = short or long or title
459 if not self.get_saved():
460 title = "*%s*" % title
461 icon = "*%s" % icon
462 self.top.wm_title(title)
463 self.top.wm_iconname(icon)
465 def get_saved(self):
466 return self.undo.get_saved()
468 def set_saved(self, flag):
469 self.undo.set_saved(flag)
471 def reset_undo(self):
472 self.undo.reset_undo()
474 def short_title(self):
475 filename = self.io.filename
476 if filename:
477 filename = os.path.basename(filename)
478 return filename
480 def long_title(self):
481 return self.io.filename or ""
483 def center_insert_event(self, event):
484 self.center()
486 def center(self, mark="insert"):
487 text = self.text
488 top, bot = self.getwindowlines()
489 lineno = self.getlineno(mark)
490 height = bot - top
491 newtop = max(1, lineno - height/2)
492 text.yview(float(newtop))
494 def getwindowlines(self):
495 text = self.text
496 top = self.getlineno("@0,0")
497 bot = self.getlineno("@0,65535")
498 if top == bot and text.winfo_height() == 1:
499 # Geometry manager hasn't run yet
500 height = int(text['height'])
501 bot = top + height - 1
502 return top, bot
504 def getlineno(self, mark="insert"):
505 text = self.text
506 return int(float(text.index(mark)))
508 def close_event(self, event):
509 self.close()
511 def maybesave(self):
512 if self.io:
513 return self.io.maybesave()
515 def close(self):
516 self.top.wm_deiconify()
517 self.top.tkraise()
518 reply = self.maybesave()
519 if reply != "cancel":
520 self._close()
521 return reply
523 def _close(self):
524 WindowList.unregister_callback(self.postwindowsmenu)
525 if self.close_hook:
526 self.close_hook()
527 self.flist = None
528 colorizing = 0
529 self.unload_extensions()
530 self.io.close(); self.io = None
531 self.undo = None # XXX
532 if self.color:
533 colorizing = self.color.colorizing
534 doh = colorizing and self.top
535 self.color.close(doh) # Cancel colorization
536 self.text = None
537 self.vars = None
538 self.per.close(); self.per = None
539 if not colorizing:
540 self.top.destroy()
542 def load_extensions(self):
543 self.extensions = {}
544 self.load_standard_extensions()
546 def unload_extensions(self):
547 for ins in self.extensions.values():
548 if hasattr(ins, "close"):
549 ins.close()
550 self.extensions = {}
552 def load_standard_extensions(self):
553 for name in self.get_standard_extension_names():
554 try:
555 self.load_extension(name)
556 except:
557 print "Failed to load extension", `name`
558 import traceback
559 traceback.print_exc()
561 def get_standard_extension_names(self):
562 return idleconf.getextensions()
564 def load_extension(self, name):
565 mod = __import__(name, globals(), locals(), [])
566 cls = getattr(mod, name)
567 ins = cls(self)
568 self.extensions[name] = ins
569 kdnames = ["keydefs"]
570 if sys.platform == 'win32':
571 kdnames.append("windows_keydefs")
572 elif sys.platform == 'mac':
573 kdnames.append("mac_keydefs")
574 else:
575 kdnames.append("unix_keydefs")
576 keydefs = {}
577 for kdname in kdnames:
578 if hasattr(ins, kdname):
579 keydefs.update(getattr(ins, kdname))
580 if keydefs:
581 self.apply_bindings(keydefs)
582 for vevent in keydefs.keys():
583 methodname = string.replace(vevent, "-", "_")
584 while methodname[:1] == '<':
585 methodname = methodname[1:]
586 while methodname[-1:] == '>':
587 methodname = methodname[:-1]
588 methodname = methodname + "_event"
589 if hasattr(ins, methodname):
590 self.text.bind(vevent, getattr(ins, methodname))
591 if hasattr(ins, "menudefs"):
592 self.fill_menus(ins.menudefs, keydefs)
593 return ins
595 def apply_bindings(self, keydefs=None):
596 if keydefs is None:
597 keydefs = self.Bindings.default_keydefs
598 text = self.text
599 text.keydefs = keydefs
600 for event, keylist in keydefs.items():
601 if keylist:
602 apply(text.event_add, (event,) + tuple(keylist))
604 def fill_menus(self, defs=None, keydefs=None):
605 # Fill the menus. Menus that are absent or None in
606 # self.menudict are ignored.
607 if defs is None:
608 defs = self.Bindings.menudefs
609 if keydefs is None:
610 keydefs = self.Bindings.default_keydefs
611 menudict = self.menudict
612 text = self.text
613 for mname, itemlist in defs:
614 menu = menudict.get(mname)
615 if not menu:
616 continue
617 for item in itemlist:
618 if not item:
619 menu.add_separator()
620 else:
621 label, event = item
622 checkbutton = (label[:1] == '!')
623 if checkbutton:
624 label = label[1:]
625 underline, label = prepstr(label)
626 accelerator = get_accelerator(keydefs, event)
627 def command(text=text, event=event):
628 text.event_generate(event)
629 if checkbutton:
630 var = self.getrawvar(event, BooleanVar)
631 menu.add_checkbutton(label=label, underline=underline,
632 command=command, accelerator=accelerator,
633 variable=var)
634 else:
635 menu.add_command(label=label, underline=underline,
636 command=command, accelerator=accelerator)
638 def getvar(self, name):
639 var = self.getrawvar(name)
640 if var:
641 return var.get()
643 def setvar(self, name, value, vartype=None):
644 var = self.getrawvar(name, vartype)
645 if var:
646 var.set(value)
648 def getrawvar(self, name, vartype=None):
649 var = self.vars.get(name)
650 if not var and vartype:
651 self.vars[name] = var = vartype(self.text)
652 return var
654 # Tk implementations of "virtual text methods" -- each platform
655 # reusing IDLE's support code needs to define these for its GUI's
656 # flavor of widget.
658 # Is character at text_index in a Python string? Return 0 for
659 # "guaranteed no", true for anything else. This info is expensive
660 # to compute ab initio, but is probably already known by the
661 # platform's colorizer.
663 def is_char_in_string(self, text_index):
664 if self.color:
665 # Return true iff colorizer hasn't (re)gotten this far
666 # yet, or the character is tagged as being in a string
667 return self.text.tag_prevrange("TODO", text_index) or \
668 "STRING" in self.text.tag_names(text_index)
669 else:
670 # The colorizer is missing: assume the worst
671 return 1
673 # If a selection is defined in the text widget, return (start,
674 # end) as Tkinter text indices, otherwise return (None, None)
675 def get_selection_indices(self):
676 try:
677 first = self.text.index("sel.first")
678 last = self.text.index("sel.last")
679 return first, last
680 except TclError:
681 return None, None
683 # Return the text widget's current view of what a tab stop means
684 # (equivalent width in spaces).
686 def get_tabwidth(self):
687 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
688 return int(current)
690 # Set the text widget's current view of what a tab stop means.
692 def set_tabwidth(self, newtabwidth):
693 text = self.text
694 if self.get_tabwidth() != newtabwidth:
695 pixels = text.tk.call("font", "measure", text["font"],
696 "-displayof", text.master,
697 "n" * newtabwidth)
698 text.configure(tabs=pixels)
700 def prepstr(s):
701 # Helper to extract the underscore from a string, e.g.
702 # prepstr("Co_py") returns (2, "Copy").
703 i = string.find(s, '_')
704 if i >= 0:
705 s = s[:i] + s[i+1:]
706 return i, s
709 keynames = {
710 'bracketleft': '[',
711 'bracketright': ']',
712 'slash': '/',
715 def get_accelerator(keydefs, event):
716 keylist = keydefs.get(event)
717 if not keylist:
718 return ""
719 s = keylist[0]
720 s = re.sub(r"-[a-z]\b", lambda m: string.upper(m.group()), s)
721 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
722 s = re.sub("Key-", "", s)
723 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
724 s = re.sub("Control-", "Ctrl-", s)
725 s = re.sub("-", "+", s)
726 s = re.sub("><", " ", s)
727 s = re.sub("<", "", s)
728 s = re.sub(">", "", s)
729 return s
732 def fixwordbreaks(root):
733 # Make sure that Tk's double-click and next/previous word
734 # operations use our definition of a word (i.e. an identifier)
735 tk = root.tk
736 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
737 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
738 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
741 def test():
742 root = Tk()
743 fixwordbreaks(root)
744 root.withdraw()
745 if sys.argv[1:]:
746 filename = sys.argv[1]
747 else:
748 filename = None
749 edit = EditorWindow(root=root, filename=filename)
750 edit.set_close_hook(root.quit)
751 root.mainloop()
752 root.destroy()
754 if __name__ == '__main__':
755 test()