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