blob: 9f76ef7a0364844689ad4ec7194676dcd2a50277 [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001# 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)
6
7import sys
8import os
9import string
10import re
11import imp
12from Tkinter import *
13import tkSimpleDialog
14import tkMessageBox
15import idlever
16import WindowList
17from IdleConf import idleconf
18
19# The default tab setting for a Text widget, in average-width characters.
20TK_TABWIDTH_DEFAULT = 8
21
22# File menu
23
24#$ event <<open-module>>
25#$ win <Alt-m>
26#$ unix <Control-x><Control-m>
27
28#$ event <<open-class-browser>>
29#$ win <Alt-c>
30#$ unix <Control-x><Control-b>
31
32#$ event <<open-path-browser>>
33
34#$ event <<close-window>>
35#$ unix <Control-x><Control-0>
36#$ unix <Control-x><Key-0>
37#$ win <Alt-F4>
38
39# Edit menu
40
41#$ event <<Copy>>
42#$ win <Control-c>
43#$ unix <Alt-w>
44
45#$ event <<Cut>>
46#$ win <Control-x>
47#$ unix <Control-w>
48
49#$ event <<Paste>>
50#$ win <Control-v>
51#$ unix <Control-y>
52
53#$ event <<select-all>>
54#$ win <Alt-a>
55#$ unix <Alt-a>
56
57# Help menu
58
59#$ event <<help>>
60#$ win <F1>
61#$ unix <F1>
62
63#$ event <<about-idle>>
64
65# Events without menu entries
66
67#$ event <<remove-selection>>
68#$ win <Escape>
69
70#$ event <<center-insert>>
71#$ win <Control-l>
72#$ unix <Control-l>
73
74#$ event <<do-nothing>>
75#$ unix <Control-x>
76
77
78about_title = "About IDLE"
79about_text = """\
80IDLE %s
81
82An Integrated DeveLopment Environment for Python
83
84by Guido van Rossum
85
86This version of IDLE has been modified by David Scherer
87 (dscherer@cmu.edu). See readme.txt for details.
88""" % idlever.IDLE_VERSION
89
90class EditorWindow:
91
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
99
100 about_title = about_title
101 about_text = about_text
102
103 vars = {}
104
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")
126
127 self.createmenubar()
128 self.apply_bindings()
129
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)
150
151 vbar['command'] = text.yview
152 vbar.pack(side=RIGHT, fill=Y)
153
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()
159
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)
169
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)
174
175 if filename:
176 if os.path.exists(filename):
177 io.loadfile(filename)
178 else:
179 io.set_filename(filename)
180
181 self.saved_change_hook()
182
183 self.load_extensions()
184
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)
195
196 # Some abstractions so IDLE extensions are cross-IDE
197 self.askyesno = tkMessageBox.askyesno
198 self.askinteger = tkSimpleDialog.askinteger
199 self.showerror = tkMessageBox.showerror
200
201 if self.extensions.has_key('AutoIndent'):
202 self.extensions['AutoIndent'].set_indentation_params(
203 self.ispythonsource(filename))
204 self.set_status_bar()
205
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)
214
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)
219
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()
226
227 menu_specs = [
228 ("file", "_File"),
229 ("edit", "_Edit"),
230 ("format", "F_ormat"),
231 ("run", "_Run"),
232 ("windows", "_Windows"),
233 ("help", "_Help"),
234 ]
235
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()
244
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)
257
258 rmenu = None
259
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")
273
274 rmenu_specs = [
275 # ("Label", "<<virtual-event>>"), ...
276 ("Close", "<<close-window>>"), # Example
277 ]
278
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
286
287 def about_dialog(self, event=None):
288 tkMessageBox.showinfo(self.about_title, self.about_text,
289 master=self.text)
290
291 helpfile = "help.txt"
292
293 def good_advice(self, event=None):
294 tkMessageBox.showinfo('Advice', "Don't Panic!", master=self.text)
295
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)
305
306 help_viewer = "netscape -remote 'openurl(%(url)s)' 2>/dev/null || " \
307 "netscape %(url)s &"
308 help_url = "http://www.python.org/doc/current/"
309
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)
322
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"
328
329 def remove_selection(self, event=None):
330 self.text.tag_remove("sel", "1.0", "end")
331 self.text.see("insert")
332
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)
367
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])
381
382 def open_path_browser(self, event=None):
383 import PathBrowser
384 PathBrowser.PathBrowser(self.flist)
385
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()
392
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
406
407 def close_hook(self):
408 if self.flist:
409 self.flist.close_edit(self)
410
411 def set_close_hook(self, close_hook):
412 self.close_hook = close_hook
413
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()
422
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)
431
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)
440
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)
458
459 def get_saved(self):
460 return self.undo.get_saved()
461
462 def set_saved(self, flag):
463 self.undo.set_saved(flag)
464
465 def reset_undo(self):
466 self.undo.reset_undo()
467
468 def short_title(self):
469 filename = self.io.filename
470 if filename:
471 filename = os.path.basename(filename)
472 return filename
473
474 def long_title(self):
475 return self.io.filename or ""
476
477 def center_insert_event(self, event):
478 self.center()
479
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))
487
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
497
498 def getlineno(self, mark="insert"):
499 text = self.text
500 return int(float(text.index(mark)))
501
502 def close_event(self, event):
503 self.close()
504
505 def maybesave(self):
506 if self.io:
507 return self.io.maybesave()
508
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
516
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()
535
536 def load_extensions(self):
537 self.extensions = {}
538 self.load_standard_extensions()
539
540 def unload_extensions(self):
541 for ins in self.extensions.values():
542 if hasattr(ins, "close"):
543 ins.close()
544 self.extensions = {}
545
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()
554
555 def get_standard_extension_names(self):
556 return idleconf.getextensions()
557
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
588
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))
597
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)
631
632 def getvar(self, name):
633 var = self.getrawvar(name)
634 if var:
635 return var.get()
636
637 def setvar(self, name, value, vartype=None):
638 var = self.getrawvar(name, vartype)
639 if var:
640 var.set(value)
641
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
647
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.
651
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.
656
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
666
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
676
677 # Return the text widget's current view of what a tab stop means
678 # (equivalent width in spaces).
679
680 def get_tabwidth(self):
681 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
682 return int(current)
683
684 # Set the text widget's current view of what a tab stop means.
685
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)
693
694def 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
701
702
703keynames = {
704 'bracketleft': '[',
705 'bracketright': ']',
706 'slash': '/',
707}
708
709def 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
724
725
726def 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_]')
733
734
735def 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()
747
748if __name__ == '__main__':
749 test()