Added 'list_only' option (and modified 'run()' to respect it).
[python/dscho.git] / Tools / idle / EditorWindow.py
blobeb9466abbc1b30e9ac7a8f3a5bcc3e54bd5338ca
1 import sys
2 import os
3 import string
4 import re
5 import imp
6 from Tkinter import *
7 import tkSimpleDialog
8 import tkMessageBox
9 import idlever
10 import WindowList
12 # The default tab setting for a Text widget, in average-width characters.
13 TK_TABWIDTH_DEFAULT = 8
15 # File menu
17 #$ event <<open-module>>
18 #$ win <Alt-m>
19 #$ unix <Control-x><Control-m>
21 #$ event <<open-class-browser>>
22 #$ win <Alt-c>
23 #$ unix <Control-x><Control-b>
25 #$ event <<open-path-browser>>
27 #$ event <<close-window>>
28 #$ unix <Control-x><Control-0>
29 #$ unix <Control-x><Key-0>
30 #$ win <Alt-F4>
32 # Edit menu
34 #$ event <<Copy>>
35 #$ win <Control-c>
36 #$ unix <Alt-w>
38 #$ event <<Cut>>
39 #$ win <Control-x>
40 #$ unix <Control-w>
42 #$ event <<Paste>>
43 #$ win <Control-v>
44 #$ unix <Control-y>
46 #$ event <<select-all>>
47 #$ win <Alt-a>
48 #$ unix <Alt-a>
50 # Help menu
52 #$ event <<help>>
53 #$ win <F1>
54 #$ unix <F1>
56 #$ event <<about-idle>>
58 # Events without menu entries
60 #$ event <<remove-selection>>
61 #$ win <Escape>
63 #$ event <<center-insert>>
64 #$ win <Control-l>
65 #$ unix <Control-l>
67 #$ event <<do-nothing>>
68 #$ unix <Control-x>
71 about_title = "About IDLE"
72 about_text = """\
73 IDLE %s
75 An Integrated DeveLopment Environment for Python
77 by Guido van Rossum
78 """ % idlever.IDLE_VERSION
80 class EditorWindow:
82 from Percolator import Percolator
83 from ColorDelegator import ColorDelegator
84 from UndoDelegator import UndoDelegator
85 from IOBinding import IOBinding
86 import Bindings
87 from Tkinter import Toplevel
89 about_title = about_title
90 about_text = about_text
92 vars = {}
94 def __init__(self, flist=None, filename=None, key=None, root=None):
95 cprefs = self.ColorDelegator.cprefs
96 self.flist = flist
97 root = root or flist.root
98 self.root = root
99 if flist:
100 self.vars = flist.vars
101 self.menubar = Menu(root)
102 self.top = top = self.Toplevel(root, menu=self.menubar)
103 self.vbar = vbar = Scrollbar(top, name='vbar')
104 self.text = text = Text(top, name='text', padx=5,
105 foreground=cprefs.CNormal[0],
106 background=cprefs.CNormal[1],
107 highlightcolor=cprefs.CHilite[0],
108 highlightbackground=cprefs.CHilite[1],
109 insertbackground=cprefs.CCursor[1],
110 wrap="none")
112 self.createmenubar()
113 self.apply_bindings()
115 self.top.protocol("WM_DELETE_WINDOW", self.close)
116 self.top.bind("<<close-window>>", self.close_event)
117 text.bind("<<center-insert>>", self.center_insert_event)
118 text.bind("<<help>>", self.help_dialog)
119 text.bind("<<python-docs>>", self.python_docs)
120 text.bind("<<about-idle>>", self.about_dialog)
121 text.bind("<<open-module>>", self.open_module)
122 text.bind("<<do-nothing>>", lambda event: "break")
123 text.bind("<<select-all>>", self.select_all)
124 text.bind("<<remove-selection>>", self.remove_selection)
125 text.bind("<3>", self.right_menu_event)
126 if flist:
127 flist.inversedict[self] = key
128 if key:
129 flist.dict[key] = self
130 text.bind("<<open-new-window>>", self.flist.new_callback)
131 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
132 text.bind("<<open-class-browser>>", self.open_class_browser)
133 text.bind("<<open-path-browser>>", self.open_path_browser)
135 vbar['command'] = text.yview
136 vbar.pack(side=RIGHT, fill=Y)
138 text['yscrollcommand'] = vbar.set
139 if sys.platform[:3] == 'win':
140 text['font'] = ("lucida console", 8)
141 # text['font'] = ("courier new", 10)
142 text.pack(side=LEFT, fill=BOTH, expand=1)
143 text.focus_set()
145 self.per = per = self.Percolator(text)
146 if self.ispythonsource(filename):
147 self.color = color = self.ColorDelegator(); per.insertfilter(color)
148 ##print "Initial colorizer"
149 else:
150 ##print "No initial colorizer"
151 self.color = None
152 self.undo = undo = self.UndoDelegator(); per.insertfilter(undo)
153 self.io = io = self.IOBinding(self)
155 text.undo_block_start = undo.undo_block_start
156 text.undo_block_stop = undo.undo_block_stop
157 undo.set_saved_change_hook(self.saved_change_hook)
158 io.set_filename_change_hook(self.filename_change_hook)
160 if filename:
161 if os.path.exists(filename):
162 io.loadfile(filename)
163 else:
164 io.set_filename(filename)
166 self.saved_change_hook()
168 self.load_extensions()
170 menu = self.menudict.get('windows')
171 if menu:
172 end = menu.index("end")
173 if end is None:
174 end = -1
175 if end >= 0:
176 menu.add_separator()
177 end = end + 1
178 self.wmenu_end = end
179 WindowList.register_callback(self.postwindowsmenu)
181 # Some abstractions so IDLE extensions are cross-IDE
182 self.askyesno = tkMessageBox.askyesno
183 self.askinteger = tkSimpleDialog.askinteger
184 self.showerror = tkMessageBox.showerror
186 if self.extensions.has_key('AutoIndent'):
187 self.extensions['AutoIndent'].set_indentation_params(
188 self.ispythonsource(filename))
190 def wakeup(self):
191 if self.top.wm_state() == "iconic":
192 self.top.wm_deiconify()
193 else:
194 self.top.tkraise()
195 self.text.focus_set()
197 menu_specs = [
198 ("file", "_File"),
199 ("edit", "_Edit"),
200 ("windows", "_Windows"),
201 ("help", "_Help"),
204 def createmenubar(self):
205 mbar = self.menubar
206 self.menudict = menudict = {}
207 for name, label in self.menu_specs:
208 underline, label = prepstr(label)
209 menudict[name] = menu = Menu(mbar, name=name)
210 mbar.add_cascade(label=label, menu=menu, underline=underline)
211 self.fill_menus()
213 def postwindowsmenu(self):
214 # Only called when Windows menu exists
215 # XXX Actually, this Just-In-Time updating interferes badly
216 # XXX with the tear-off feature. It would be better to update
217 # XXX all Windows menus whenever the list of windows changes.
218 menu = self.menudict['windows']
219 end = menu.index("end")
220 if end is None:
221 end = -1
222 if end > self.wmenu_end:
223 menu.delete(self.wmenu_end+1, end)
224 WindowList.add_windows_to_menu(menu)
226 rmenu = None
228 def right_menu_event(self, event):
229 self.text.tag_remove("sel", "1.0", "end")
230 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
231 if not self.rmenu:
232 self.make_rmenu()
233 rmenu = self.rmenu
234 self.event = event
235 iswin = sys.platform[:3] == 'win'
236 if iswin:
237 self.text.config(cursor="arrow")
238 rmenu.tk_popup(event.x_root, event.y_root)
239 if iswin:
240 self.text.config(cursor="ibeam")
242 rmenu_specs = [
243 # ("Label", "<<virtual-event>>"), ...
244 ("Close", "<<close-window>>"), # Example
247 def make_rmenu(self):
248 rmenu = Menu(self.text, tearoff=0)
249 for label, eventname in self.rmenu_specs:
250 def command(text=self.text, eventname=eventname):
251 text.event_generate(eventname)
252 rmenu.add_command(label=label, command=command)
253 self.rmenu = rmenu
255 def about_dialog(self, event=None):
256 tkMessageBox.showinfo(self.about_title, self.about_text,
257 master=self.text)
259 helpfile = "help.txt"
261 def help_dialog(self, event=None):
262 try:
263 helpfile = os.path.join(os.path.dirname(__file__), self.helpfile)
264 except NameError:
265 helpfile = self.helpfile
266 if self.flist:
267 self.flist.open(helpfile)
268 else:
269 self.io.loadfile(helpfile)
271 # XXX Fix these for Windows
272 help_viewer = "netscape -remote 'openurl(%(url)s)' 2>/dev/null || " \
273 "netscape %(url)s &"
274 help_url = "http://www.python.org/doc/current/"
276 def python_docs(self, event=None):
277 cmd = self.help_viewer % {"url": self.help_url}
278 os.system(cmd)
280 def select_all(self, event=None):
281 self.text.tag_add("sel", "1.0", "end-1c")
282 self.text.mark_set("insert", "1.0")
283 self.text.see("insert")
284 return "break"
286 def remove_selection(self, event=None):
287 self.text.tag_remove("sel", "1.0", "end")
288 self.text.see("insert")
290 def open_module(self, event=None):
291 # XXX Shouldn't this be in IOBinding or in FileList?
292 try:
293 name = self.text.get("sel.first", "sel.last")
294 except TclError:
295 name = ""
296 else:
297 name = string.strip(name)
298 if not name:
299 name = tkSimpleDialog.askstring("Module",
300 "Enter the name of a Python module\n"
301 "to search on sys.path and open:",
302 parent=self.text)
303 if name:
304 name = string.strip(name)
305 if not name:
306 return
307 # XXX Ought to support package syntax
308 # XXX Ought to insert current file's directory in front of path
309 try:
310 (f, file, (suffix, mode, type)) = imp.find_module(name)
311 except (NameError, ImportError), msg:
312 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
313 return
314 if type != imp.PY_SOURCE:
315 tkMessageBox.showerror("Unsupported type",
316 "%s is not a source module" % name, parent=self.text)
317 return
318 if f:
319 f.close()
320 if self.flist:
321 self.flist.open(file)
322 else:
323 self.io.loadfile(file)
325 def open_class_browser(self, event=None):
326 filename = self.io.filename
327 if not filename:
328 tkMessageBox.showerror(
329 "No filename",
330 "This buffer has no associated filename",
331 master=self.text)
332 self.text.focus_set()
333 return None
334 head, tail = os.path.split(filename)
335 base, ext = os.path.splitext(tail)
336 import ClassBrowser
337 ClassBrowser.ClassBrowser(self.flist, base, [head])
339 def open_path_browser(self, event=None):
340 import PathBrowser
341 PathBrowser.PathBrowser(self.flist)
343 def gotoline(self, lineno):
344 if lineno is not None and lineno > 0:
345 self.text.mark_set("insert", "%d.0" % lineno)
346 self.text.tag_remove("sel", "1.0", "end")
347 self.text.tag_add("sel", "insert", "insert +1l")
348 self.center()
350 def ispythonsource(self, filename):
351 if not filename:
352 return 1
353 base, ext = os.path.splitext(os.path.basename(filename))
354 if os.path.normcase(ext) in (".py", ".pyw"):
355 return 1
356 try:
357 f = open(filename)
358 line = f.readline()
359 f.close()
360 except IOError:
361 return 0
362 return line[:2] == '#!' and string.find(line, 'python') >= 0
364 def close_hook(self):
365 if self.flist:
366 self.flist.close_edit(self)
368 def set_close_hook(self, close_hook):
369 self.close_hook = close_hook
371 def filename_change_hook(self):
372 if self.flist:
373 self.flist.filename_changed_edit(self)
374 self.saved_change_hook()
375 if self.ispythonsource(self.io.filename):
376 self.addcolorizer()
377 else:
378 self.rmcolorizer()
380 def addcolorizer(self):
381 if self.color:
382 return
383 ##print "Add colorizer"
384 self.per.removefilter(self.undo)
385 self.color = self.ColorDelegator()
386 self.per.insertfilter(self.color)
387 self.per.insertfilter(self.undo)
389 def rmcolorizer(self):
390 if not self.color:
391 return
392 ##print "Remove colorizer"
393 self.per.removefilter(self.undo)
394 self.per.removefilter(self.color)
395 self.color = None
396 self.per.insertfilter(self.undo)
398 def saved_change_hook(self):
399 short = self.short_title()
400 long = self.long_title()
401 if short and long:
402 title = short + " - " + long
403 elif short:
404 title = short
405 elif long:
406 title = long
407 else:
408 title = "Untitled"
409 icon = short or long or title
410 if not self.get_saved():
411 title = "*%s*" % title
412 icon = "*%s" % icon
413 self.top.wm_title(title)
414 self.top.wm_iconname(icon)
416 def get_saved(self):
417 return self.undo.get_saved()
419 def set_saved(self, flag):
420 self.undo.set_saved(flag)
422 def reset_undo(self):
423 self.undo.reset_undo()
425 def short_title(self):
426 filename = self.io.filename
427 if filename:
428 filename = os.path.basename(filename)
429 return filename
431 def long_title(self):
432 return self.io.filename or ""
434 def center_insert_event(self, event):
435 self.center()
437 def center(self, mark="insert"):
438 text = self.text
439 top, bot = self.getwindowlines()
440 lineno = self.getlineno(mark)
441 height = bot - top
442 newtop = max(1, lineno - height/2)
443 text.yview(float(newtop))
445 def getwindowlines(self):
446 text = self.text
447 top = self.getlineno("@0,0")
448 bot = self.getlineno("@0,65535")
449 if top == bot and text.winfo_height() == 1:
450 # Geometry manager hasn't run yet
451 height = int(text['height'])
452 bot = top + height - 1
453 return top, bot
455 def getlineno(self, mark="insert"):
456 text = self.text
457 return int(float(text.index(mark)))
459 def close_event(self, event):
460 self.close()
462 def maybesave(self):
463 if self.io:
464 return self.io.maybesave()
466 def close(self):
467 self.top.wm_deiconify()
468 self.top.tkraise()
469 reply = self.maybesave()
470 if reply != "cancel":
471 self._close()
472 return reply
474 def _close(self):
475 WindowList.unregister_callback(self.postwindowsmenu)
476 if self.close_hook:
477 self.close_hook()
478 self.flist = None
479 colorizing = 0
480 self.unload_extensions()
481 self.io.close(); self.io = None
482 self.undo = None # XXX
483 if self.color:
484 colorizing = self.color.colorizing
485 doh = colorizing and self.top
486 self.color.close(doh) # Cancel colorization
487 self.text = None
488 self.vars = None
489 self.per.close(); self.per = None
490 if not colorizing:
491 self.top.destroy()
493 def load_extensions(self):
494 self.extensions = {}
495 self.load_standard_extensions()
497 def unload_extensions(self):
498 for ins in self.extensions.values():
499 if hasattr(ins, "close"):
500 ins.close()
501 self.extensions = {}
503 def load_standard_extensions(self):
504 for name in self.get_standard_extension_names():
505 try:
506 self.load_extension(name)
507 except:
508 print "Failed to load extension", `name`
509 import traceback
510 traceback.print_exc()
512 def get_standard_extension_names(self):
513 import extend
514 return extend.standard
516 def load_extension(self, name):
517 mod = __import__(name, globals(), locals(), [])
518 cls = getattr(mod, name)
519 ins = cls(self)
520 self.extensions[name] = ins
521 kdnames = ["keydefs"]
522 if sys.platform == 'win32':
523 kdnames.append("windows_keydefs")
524 elif sys.platform == 'mac':
525 kdnames.append("mac_keydefs")
526 else:
527 kdnames.append("unix_keydefs")
528 keydefs = {}
529 for kdname in kdnames:
530 if hasattr(ins, kdname):
531 keydefs.update(getattr(ins, kdname))
532 if keydefs:
533 self.apply_bindings(keydefs)
534 for vevent in keydefs.keys():
535 methodname = string.replace(vevent, "-", "_")
536 while methodname[:1] == '<':
537 methodname = methodname[1:]
538 while methodname[-1:] == '>':
539 methodname = methodname[:-1]
540 methodname = methodname + "_event"
541 if hasattr(ins, methodname):
542 self.text.bind(vevent, getattr(ins, methodname))
543 if hasattr(ins, "menudefs"):
544 self.fill_menus(ins.menudefs, keydefs)
545 return ins
547 def apply_bindings(self, keydefs=None):
548 if keydefs is None:
549 keydefs = self.Bindings.default_keydefs
550 text = self.text
551 text.keydefs = keydefs
552 for event, keylist in keydefs.items():
553 if keylist:
554 apply(text.event_add, (event,) + tuple(keylist))
556 def fill_menus(self, defs=None, keydefs=None):
557 # Fill the menus. Menus that are absent or None in
558 # self.menudict are ignored.
559 if defs is None:
560 defs = self.Bindings.menudefs
561 if keydefs is None:
562 keydefs = self.Bindings.default_keydefs
563 menudict = self.menudict
564 text = self.text
565 for mname, itemlist in defs:
566 menu = menudict.get(mname)
567 if not menu:
568 continue
569 for item in itemlist:
570 if not item:
571 menu.add_separator()
572 else:
573 label, event = item
574 checkbutton = (label[:1] == '!')
575 if checkbutton:
576 label = label[1:]
577 underline, label = prepstr(label)
578 accelerator = get_accelerator(keydefs, event)
579 def command(text=text, event=event):
580 text.event_generate(event)
581 if checkbutton:
582 var = self.getrawvar(event, BooleanVar)
583 menu.add_checkbutton(label=label, underline=underline,
584 command=command, accelerator=accelerator,
585 variable=var)
586 else:
587 menu.add_command(label=label, underline=underline,
588 command=command, accelerator=accelerator)
590 def getvar(self, name):
591 var = self.getrawvar(name)
592 if var:
593 return var.get()
595 def setvar(self, name, value, vartype=None):
596 var = self.getrawvar(name, vartype)
597 if var:
598 var.set(value)
600 def getrawvar(self, name, vartype=None):
601 var = self.vars.get(name)
602 if not var and vartype:
603 self.vars[name] = var = vartype(self.text)
604 return var
606 # Tk implementations of "virtual text methods" -- each platform
607 # reusing IDLE's support code needs to define these for its GUI's
608 # flavor of widget.
610 # Is character at text_index in a Python string? Return 0 for
611 # "guaranteed no", true for anything else. This info is expensive
612 # to compute ab initio, but is probably already known by the
613 # platform's colorizer.
615 def is_char_in_string(self, text_index):
616 if self.color:
617 # Return true iff colorizer hasn't (re)gotten this far
618 # yet, or the character is tagged as being in a string
619 return self.text.tag_prevrange("TODO", text_index) or \
620 "STRING" in self.text.tag_names(text_index)
621 else:
622 # The colorizer is missing: assume the worst
623 return 1
625 # If a selection is defined in the text widget, return (start,
626 # end) as Tkinter text indices, otherwise return (None, None)
627 def get_selection_indices(self):
628 try:
629 first = self.text.index("sel.first")
630 last = self.text.index("sel.last")
631 return first, last
632 except TclError:
633 return None, None
635 # Return the text widget's current view of what a tab stop means
636 # (equivalent width in spaces).
638 def get_tabwidth(self):
639 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
640 return int(current)
642 # Set the text widget's current view of what a tab stop means.
644 def set_tabwidth(self, newtabwidth):
645 text = self.text
646 if self.get_tabwidth() != newtabwidth:
647 pixels = text.tk.call("font", "measure", text["font"],
648 "-displayof", text.master,
649 "n" * newtabwith)
650 text.configure(tabs=pixels)
652 def prepstr(s):
653 # Helper to extract the underscore from a string, e.g.
654 # prepstr("Co_py") returns (2, "Copy").
655 i = string.find(s, '_')
656 if i >= 0:
657 s = s[:i] + s[i+1:]
658 return i, s
661 keynames = {
662 'bracketleft': '[',
663 'bracketright': ']',
664 'slash': '/',
667 def get_accelerator(keydefs, event):
668 keylist = keydefs.get(event)
669 if not keylist:
670 return ""
671 s = keylist[0]
672 s = re.sub(r"-[a-z]\b", lambda m: string.upper(m.group()), s)
673 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
674 s = re.sub("Key-", "", s)
675 s = re.sub("Control-", "Ctrl-", s)
676 s = re.sub("-", "+", s)
677 s = re.sub("><", " ", s)
678 s = re.sub("<", "", s)
679 s = re.sub(">", "", s)
680 return s
683 def fixwordbreaks(root):
684 # Make sure that Tk's double-click and next/previous word
685 # operations use our definition of a word (i.e. an identifier)
686 tk = root.tk
687 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
688 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
689 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
692 def test():
693 root = Tk()
694 fixwordbreaks(root)
695 root.withdraw()
696 if sys.argv[1:]:
697 filename = sys.argv[1]
698 else:
699 filename = None
700 edit = EditorWindow(root=root, filename=filename)
701 edit.set_close_hook(root.quit)
702 root.mainloop()
703 root.destroy()
705 if __name__ == '__main__':
706 test()