1 # 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)
19 from IdleConf
import idleconf
21 # The default tab setting for a Text widget, in average-width characters.
22 TK_TABWIDTH_DEFAULT
= 8
26 #$ event <<open-module>>
28 #$ unix <Control-x><Control-m>
30 #$ event <<open-class-browser>>
32 #$ unix <Control-x><Control-b>
34 #$ event <<open-path-browser>>
36 #$ event <<close-window>>
38 #$ unix <Control-x><Control-0>
39 #$ unix <Control-x><Key-0>
56 #$ event <<select-all>>
66 #$ event <<about-idle>>
68 # Events without menu entries
70 #$ event <<remove-selection>>
73 #$ event <<center-insert>>
77 #$ event <<do-nothing>>
81 about_title
= "About IDLEfork"
85 IDLE is an Integrated DeveLopment Environment for Python \
88 IDLEfork is an official experimental development version of IDLE.
89 Succesful new features in IDLEfork will be mereged back in to stable IDLE.
91 This version of IDLEfork is based on the work in stable IDLE version %s, \
92 IDLEfork 0.7.1 released by David Scherer, and the VPython idle fork.
94 See README.txt for more details on IDLEfork.
96 WARNING: IDLEfork is at this stage alpha quality software, expect things \
99 """ % (idlever
.IDLEFORK_VERSION
, idlever
.IDLE_VERSION
)
103 from Percolator
import Percolator
104 from ColorDelegator
import ColorDelegator
105 from UndoDelegator
import UndoDelegator
106 from IOBinding
import IOBinding
108 from Tkinter
import Toplevel
109 from MultiStatusBar
import MultiStatusBar
111 about_title
= about_title
112 about_text
= about_text
116 def __init__(self
, flist
=None, filename
=None, key
=None, root
=None):
117 edconf
= idleconf
.getsection('EditorWindow')
118 coconf
= idleconf
.getsection('Colors')
120 root
= root
or flist
.root
123 self
.vars = flist
.vars
124 self
.menubar
= Menu(root
)
125 self
.top
= top
= self
.Toplevel(root
, menu
=self
.menubar
)
126 self
.vbar
= vbar
= Scrollbar(top
, name
='vbar')
127 self
.text_frame
= text_frame
= Frame(top
)
128 self
.text
= text
= Text(text_frame
, name
='text', padx
=5,
129 foreground
=coconf
.getdef('normal-foreground'),
130 background
=coconf
.getdef('normal-background'),
131 highlightcolor
=coconf
.getdef('hilite-foreground'),
132 highlightbackground
=coconf
.getdef('hilite-background'),
133 insertbackground
=coconf
.getdef('cursor-background'),
134 width
=edconf
.getint('width'),
135 height
=edconf
.getint('height'),
139 self
.apply_bindings()
141 self
.top
.protocol("WM_DELETE_WINDOW", self
.close
)
142 self
.top
.bind("<<close-window>>", self
.close_event
)
143 text
.bind("<<center-insert>>", self
.center_insert_event
)
144 text
.bind("<<help>>", self
.help_dialog
)
145 text
.bind("<<good-advice>>", self
.good_advice
)
146 text
.bind("<<python-docs>>", self
.python_docs
)
147 text
.bind("<<about-idle>>", self
.about_dialog
)
148 text
.bind("<<open-module>>", self
.open_module
)
149 text
.bind("<<do-nothing>>", lambda event
: "break")
150 text
.bind("<<select-all>>", self
.select_all
)
151 text
.bind("<<remove-selection>>", self
.remove_selection
)
152 text
.bind("<3>", self
.right_menu_event
)
154 flist
.inversedict
[self
] = key
156 flist
.dict[key
] = self
157 text
.bind("<<open-new-window>>", self
.flist
.new_callback
)
158 text
.bind("<<close-all-windows>>", self
.flist
.close_all_callback
)
159 text
.bind("<<open-class-browser>>", self
.open_class_browser
)
160 text
.bind("<<open-path-browser>>", self
.open_path_browser
)
162 vbar
['command'] = text
.yview
163 vbar
.pack(side
=RIGHT
, fill
=Y
)
165 text
['yscrollcommand'] = vbar
.set
166 text
['font'] = edconf
.get('font-name'), edconf
.get('font-size')
167 text_frame
.pack(side
=LEFT
, fill
=BOTH
, expand
=1)
168 text
.pack(side
=TOP
, fill
=BOTH
, expand
=1)
171 self
.per
= per
= self
.Percolator(text
)
172 if self
.ispythonsource(filename
):
173 self
.color
= color
= self
.ColorDelegator(); per
.insertfilter(color
)
174 ##print "Initial colorizer"
176 ##print "No initial colorizer"
178 self
.undo
= undo
= self
.UndoDelegator(); per
.insertfilter(undo
)
179 self
.io
= io
= self
.IOBinding(self
)
181 text
.undo_block_start
= undo
.undo_block_start
182 text
.undo_block_stop
= undo
.undo_block_stop
183 undo
.set_saved_change_hook(self
.saved_change_hook
)
184 io
.set_filename_change_hook(self
.filename_change_hook
)
187 if os
.path
.exists(filename
):
188 io
.loadfile(filename
)
190 io
.set_filename(filename
)
192 self
.saved_change_hook()
194 self
.load_extensions()
196 menu
= self
.menudict
.get('windows')
198 end
= menu
.index("end")
205 WindowList
.register_callback(self
.postwindowsmenu
)
207 # Some abstractions so IDLE extensions are cross-IDE
208 self
.askyesno
= tkMessageBox
.askyesno
209 self
.askinteger
= tkSimpleDialog
.askinteger
210 self
.showerror
= tkMessageBox
.showerror
212 if self
.extensions
.has_key('AutoIndent'):
213 self
.extensions
['AutoIndent'].set_indentation_params(
214 self
.ispythonsource(filename
))
215 self
.set_status_bar()
217 def set_status_bar(self
):
218 self
.status_bar
= self
.MultiStatusBar(self
.text_frame
)
219 self
.status_bar
.set_label('column', 'Col: ?', side
=RIGHT
)
220 self
.status_bar
.set_label('line', 'Ln: ?', side
=RIGHT
)
221 self
.status_bar
.pack(side
=BOTTOM
, fill
=X
)
222 self
.text
.bind('<KeyRelease>', self
.set_line_and_column
)
223 self
.text
.bind('<ButtonRelease>', self
.set_line_and_column
)
224 self
.text
.after_idle(self
.set_line_and_column
)
226 def set_line_and_column(self
, event
=None):
227 line
, column
= string
.split(self
.text
.index(INSERT
), '.')
228 self
.status_bar
.set_label('column', 'Col: %s' % column
)
229 self
.status_bar
.set_label('line', 'Ln: %s' % line
)
232 if self
.top
.wm_state() == "iconic":
233 self
.top
.wm_deiconify()
236 self
.text
.focus_set()
241 ("format", "F_ormat"),
243 ("windows", "_Windows"),
247 def createmenubar(self
):
249 self
.menudict
= menudict
= {}
250 for name
, label
in self
.menu_specs
:
251 underline
, label
= prepstr(label
)
252 menudict
[name
] = menu
= Menu(mbar
, name
=name
)
253 mbar
.add_cascade(label
=label
, menu
=menu
, underline
=underline
)
256 def postwindowsmenu(self
):
257 # Only called when Windows menu exists
258 # XXX Actually, this Just-In-Time updating interferes badly
259 # XXX with the tear-off feature. It would be better to update
260 # XXX all Windows menus whenever the list of windows changes.
261 menu
= self
.menudict
['windows']
262 end
= menu
.index("end")
265 if end
> self
.wmenu_end
:
266 menu
.delete(self
.wmenu_end
+1, end
)
267 WindowList
.add_windows_to_menu(menu
)
271 def right_menu_event(self
, event
):
272 self
.text
.tag_remove("sel", "1.0", "end")
273 self
.text
.mark_set("insert", "@%d,%d" % (event
.x
, event
.y
))
278 iswin
= sys
.platform
[:3] == 'win'
280 self
.text
.config(cursor
="arrow")
281 rmenu
.tk_popup(event
.x_root
, event
.y_root
)
283 self
.text
.config(cursor
="ibeam")
286 # ("Label", "<<virtual-event>>"), ...
287 ("Close", "<<close-window>>"), # Example
290 def make_rmenu(self
):
291 rmenu
= Menu(self
.text
, tearoff
=0)
292 for label
, eventname
in self
.rmenu_specs
:
293 def command(text
=self
.text
, eventname
=eventname
):
294 text
.event_generate(eventname
)
295 rmenu
.add_command(label
=label
, command
=command
)
298 def about_dialog(self
, event
=None):
299 tkMessageBox
.showinfo(self
.about_title
, self
.about_text
,
302 helpfile
= "help.txt"
304 def good_advice(self
, event
=None):
305 tkMessageBox
.showinfo('Advice', "Don't Panic!", master
=self
.text
)
307 def help_dialog(self
, event
=None):
309 helpfile
= os
.path
.join(os
.path
.dirname(__file__
), self
.helpfile
)
311 helpfile
= self
.helpfile
313 self
.flist
.open(helpfile
)
315 self
.io
.loadfile(helpfile
)
317 help_url
= "http://www.python.org/doc/current/"
318 if sys
.platform
[:3] == "win":
319 fn
= os
.path
.dirname(__file__
)
320 fn
= os
.path
.join(fn
, os
.pardir
, os
.pardir
, "Doc", "index.html")
321 fn
= os
.path
.normpath(fn
)
322 if os
.path
.isfile(fn
):
326 def python_docs(self
, event
=None):
327 webbrowser
.open(self
.help_url
)
329 def select_all(self
, event
=None):
330 self
.text
.tag_add("sel", "1.0", "end-1c")
331 self
.text
.mark_set("insert", "1.0")
332 self
.text
.see("insert")
335 def remove_selection(self
, event
=None):
336 self
.text
.tag_remove("sel", "1.0", "end")
337 self
.text
.see("insert")
339 def open_module(self
, event
=None):
340 # XXX Shouldn't this be in IOBinding or in FileList?
342 name
= self
.text
.get("sel.first", "sel.last")
346 name
= string
.strip(name
)
348 name
= tkSimpleDialog
.askstring("Module",
349 "Enter the name of a Python module\n"
350 "to search on sys.path and open:",
353 name
= string
.strip(name
)
356 # XXX Ought to support package syntax
357 # XXX Ought to insert current file's directory in front of path
359 (f
, file, (suffix
, mode
, type)) = imp
.find_module(name
)
360 except (NameError, ImportError), msg
:
361 tkMessageBox
.showerror("Import error", str(msg
), parent
=self
.text
)
363 if type != imp
.PY_SOURCE
:
364 tkMessageBox
.showerror("Unsupported type",
365 "%s is not a source module" % name
, parent
=self
.text
)
370 self
.flist
.open(file)
372 self
.io
.loadfile(file)
374 def open_class_browser(self
, event
=None):
375 filename
= self
.io
.filename
377 tkMessageBox
.showerror(
379 "This buffer has no associated filename",
381 self
.text
.focus_set()
383 head
, tail
= os
.path
.split(filename
)
384 base
, ext
= os
.path
.splitext(tail
)
386 ClassBrowser
.ClassBrowser(self
.flist
, base
, [head
])
388 def open_path_browser(self
, event
=None):
390 PathBrowser
.PathBrowser(self
.flist
)
392 def gotoline(self
, lineno
):
393 if lineno
is not None and lineno
> 0:
394 self
.text
.mark_set("insert", "%d.0" % lineno
)
395 self
.text
.tag_remove("sel", "1.0", "end")
396 self
.text
.tag_add("sel", "insert", "insert +1l")
399 def ispythonsource(self
, filename
):
402 base
, ext
= os
.path
.splitext(os
.path
.basename(filename
))
403 if os
.path
.normcase(ext
) in (".py", ".pyw"):
411 return line
[:2] == '#!' and string
.find(line
, 'python') >= 0
413 def close_hook(self
):
415 self
.flist
.close_edit(self
)
417 def set_close_hook(self
, close_hook
):
418 self
.close_hook
= close_hook
420 def filename_change_hook(self
):
422 self
.flist
.filename_changed_edit(self
)
423 self
.saved_change_hook()
424 if self
.ispythonsource(self
.io
.filename
):
429 def addcolorizer(self
):
432 ##print "Add colorizer"
433 self
.per
.removefilter(self
.undo
)
434 self
.color
= self
.ColorDelegator()
435 self
.per
.insertfilter(self
.color
)
436 self
.per
.insertfilter(self
.undo
)
438 def rmcolorizer(self
):
441 ##print "Remove colorizer"
442 self
.per
.removefilter(self
.undo
)
443 self
.per
.removefilter(self
.color
)
445 self
.per
.insertfilter(self
.undo
)
447 def saved_change_hook(self
):
448 short
= self
.short_title()
449 long = self
.long_title()
451 title
= short
+ " - " + long
458 icon
= short
or long or title
459 if not self
.get_saved():
460 title
= "*%s*" % title
462 self
.top
.wm_title(title
)
463 self
.top
.wm_iconname(icon
)
466 return self
.undo
.get_saved()
468 def set_saved(self
, flag
):
469 self
.undo
.set_saved(flag
)
471 def reset_undo(self
):
472 self
.undo
.reset_undo()
474 def short_title(self
):
475 filename
= self
.io
.filename
477 filename
= os
.path
.basename(filename
)
480 def long_title(self
):
481 return self
.io
.filename
or ""
483 def center_insert_event(self
, event
):
486 def center(self
, mark
="insert"):
488 top
, bot
= self
.getwindowlines()
489 lineno
= self
.getlineno(mark
)
491 newtop
= max(1, lineno
- height
/2)
492 text
.yview(float(newtop
))
494 def getwindowlines(self
):
496 top
= self
.getlineno("@0,0")
497 bot
= self
.getlineno("@0,65535")
498 if top
== bot
and text
.winfo_height() == 1:
499 # Geometry manager hasn't run yet
500 height
= int(text
['height'])
501 bot
= top
+ height
- 1
504 def getlineno(self
, mark
="insert"):
506 return int(float(text
.index(mark
)))
508 def close_event(self
, event
):
513 return self
.io
.maybesave()
516 self
.top
.wm_deiconify()
518 reply
= self
.maybesave()
519 if reply
!= "cancel":
524 WindowList
.unregister_callback(self
.postwindowsmenu
)
529 self
.unload_extensions()
530 self
.io
.close(); self
.io
= None
531 self
.undo
= None # XXX
533 colorizing
= self
.color
.colorizing
534 doh
= colorizing
and self
.top
535 self
.color
.close(doh
) # Cancel colorization
538 self
.per
.close(); self
.per
= None
542 def load_extensions(self
):
544 self
.load_standard_extensions()
546 def unload_extensions(self
):
547 for ins
in self
.extensions
.values():
548 if hasattr(ins
, "close"):
552 def load_standard_extensions(self
):
553 for name
in self
.get_standard_extension_names():
555 self
.load_extension(name
)
557 print "Failed to load extension", `name`
559 traceback
.print_exc()
561 def get_standard_extension_names(self
):
562 return idleconf
.getextensions()
564 def load_extension(self
, name
):
565 mod
= __import__(name
, globals(), locals(), [])
566 cls
= getattr(mod
, name
)
568 self
.extensions
[name
] = ins
569 kdnames
= ["keydefs"]
570 if sys
.platform
== 'win32':
571 kdnames
.append("windows_keydefs")
572 elif sys
.platform
== 'mac':
573 kdnames
.append("mac_keydefs")
575 kdnames
.append("unix_keydefs")
577 for kdname
in kdnames
:
578 if hasattr(ins
, kdname
):
579 keydefs
.update(getattr(ins
, kdname
))
581 self
.apply_bindings(keydefs
)
582 for vevent
in keydefs
.keys():
583 methodname
= string
.replace(vevent
, "-", "_")
584 while methodname
[:1] == '<':
585 methodname
= methodname
[1:]
586 while methodname
[-1:] == '>':
587 methodname
= methodname
[:-1]
588 methodname
= methodname
+ "_event"
589 if hasattr(ins
, methodname
):
590 self
.text
.bind(vevent
, getattr(ins
, methodname
))
591 if hasattr(ins
, "menudefs"):
592 self
.fill_menus(ins
.menudefs
, keydefs
)
595 def apply_bindings(self
, keydefs
=None):
597 keydefs
= self
.Bindings
.default_keydefs
599 text
.keydefs
= keydefs
600 for event
, keylist
in keydefs
.items():
602 apply(text
.event_add
, (event
,) + tuple(keylist
))
604 def fill_menus(self
, defs
=None, keydefs
=None):
605 # Fill the menus. Menus that are absent or None in
606 # self.menudict are ignored.
608 defs
= self
.Bindings
.menudefs
610 keydefs
= self
.Bindings
.default_keydefs
611 menudict
= self
.menudict
613 for mname
, itemlist
in defs
:
614 menu
= menudict
.get(mname
)
617 for item
in itemlist
:
622 checkbutton
= (label
[:1] == '!')
625 underline
, label
= prepstr(label
)
626 accelerator
= get_accelerator(keydefs
, event
)
627 def command(text
=text
, event
=event
):
628 text
.event_generate(event
)
630 var
= self
.getrawvar(event
, BooleanVar
)
631 menu
.add_checkbutton(label
=label
, underline
=underline
,
632 command
=command
, accelerator
=accelerator
,
635 menu
.add_command(label
=label
, underline
=underline
,
636 command
=command
, accelerator
=accelerator
)
638 def getvar(self
, name
):
639 var
= self
.getrawvar(name
)
643 def setvar(self
, name
, value
, vartype
=None):
644 var
= self
.getrawvar(name
, vartype
)
648 def getrawvar(self
, name
, vartype
=None):
649 var
= self
.vars.get(name
)
650 if not var
and vartype
:
651 self
.vars[name
] = var
= vartype(self
.text
)
654 # Tk implementations of "virtual text methods" -- each platform
655 # reusing IDLE's support code needs to define these for its GUI's
658 # Is character at text_index in a Python string? Return 0 for
659 # "guaranteed no", true for anything else. This info is expensive
660 # to compute ab initio, but is probably already known by the
661 # platform's colorizer.
663 def is_char_in_string(self
, text_index
):
665 # Return true iff colorizer hasn't (re)gotten this far
666 # yet, or the character is tagged as being in a string
667 return self
.text
.tag_prevrange("TODO", text_index
) or \
668 "STRING" in self
.text
.tag_names(text_index
)
670 # The colorizer is missing: assume the worst
673 # If a selection is defined in the text widget, return (start,
674 # end) as Tkinter text indices, otherwise return (None, None)
675 def get_selection_indices(self
):
677 first
= self
.text
.index("sel.first")
678 last
= self
.text
.index("sel.last")
683 # Return the text widget's current view of what a tab stop means
684 # (equivalent width in spaces).
686 def get_tabwidth(self
):
687 current
= self
.text
['tabs'] or TK_TABWIDTH_DEFAULT
690 # Set the text widget's current view of what a tab stop means.
692 def set_tabwidth(self
, newtabwidth
):
694 if self
.get_tabwidth() != newtabwidth
:
695 pixels
= text
.tk
.call("font", "measure", text
["font"],
696 "-displayof", text
.master
,
698 text
.configure(tabs
=pixels
)
701 # Helper to extract the underscore from a string, e.g.
702 # prepstr("Co_py") returns (2, "Copy").
703 i
= string
.find(s
, '_')
715 def get_accelerator(keydefs
, event
):
716 keylist
= keydefs
.get(event
)
720 s
= re
.sub(r
"-[a-z]\b", lambda m
: string
.upper(m
.group()), s
)
721 s
= re
.sub(r
"\b\w+\b", lambda m
: keynames
.get(m
.group(), m
.group()), s
)
722 s
= re
.sub("Key-", "", s
)
723 s
= re
.sub("Cancel","Ctrl-Break",s
) # dscherer@cmu.edu
724 s
= re
.sub("Control-", "Ctrl-", s
)
725 s
= re
.sub("-", "+", s
)
726 s
= re
.sub("><", " ", s
)
727 s
= re
.sub("<", "", s
)
728 s
= re
.sub(">", "", s
)
732 def fixwordbreaks(root
):
733 # Make sure that Tk's double-click and next/previous word
734 # operations use our definition of a word (i.e. an identifier)
736 tk
.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
737 tk
.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
738 tk
.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
746 filename
= sys
.argv
[1]
749 edit
= EditorWindow(root
=root
, filename
=filename
)
750 edit
.set_close_hook(root
.quit
)
754 if __name__
== '__main__':