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