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