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