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)
17 from IdleConf
import idleconf
19 # The default tab setting for a Text widget, in average-width characters.
20 TK_TABWIDTH_DEFAULT
= 8
24 #$ event <<open-module>>
26 #$ unix <Control-x><Control-m>
28 #$ event <<open-class-browser>>
30 #$ unix <Control-x><Control-b>
32 #$ event <<open-path-browser>>
34 #$ event <<close-window>>
35 #$ unix <Control-x><Control-0>
36 #$ unix <Control-x><Key-0>
53 #$ event <<select-all>>
63 #$ event <<about-idle>>
65 # Events without menu entries
67 #$ event <<remove-selection>>
70 #$ event <<center-insert>>
74 #$ event <<do-nothing>>
78 about_title
= "About IDLE"
82 An Integrated DeveLopment Environment for Python
86 This version of IDLE has been modified by David Scherer
87 (dscherer@cmu.edu). See readme.txt for details.
88 """ % idlever
.IDLE_VERSION
92 from Percolator
import Percolator
93 from ColorDelegator
import ColorDelegator
94 from UndoDelegator
import UndoDelegator
95 from IOBinding
import IOBinding
97 from Tkinter
import Toplevel
98 from MultiStatusBar
import MultiStatusBar
100 about_title
= about_title
101 about_text
= about_text
105 def __init__(self
, flist
=None, filename
=None, key
=None, root
=None):
106 edconf
= idleconf
.getsection('EditorWindow')
107 coconf
= idleconf
.getsection('Colors')
109 root
= root
or flist
.root
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'),
128 self
.apply_bindings()
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
)
143 flist
.inversedict
[self
] = 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
)
151 vbar
['command'] = text
.yview
152 vbar
.pack(side
=RIGHT
, fill
=Y
)
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)
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"
165 ##print "No initial colorizer"
167 self
.undo
= undo
= self
.UndoDelegator(); per
.insertfilter(undo
)
168 self
.io
= io
= self
.IOBinding(self
)
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
)
176 if os
.path
.exists(filename
):
177 io
.loadfile(filename
)
179 io
.set_filename(filename
)
181 self
.saved_change_hook()
183 self
.load_extensions()
185 menu
= self
.menudict
.get('windows')
187 end
= menu
.index("end")
194 WindowList
.register_callback(self
.postwindowsmenu
)
196 # Some abstractions so IDLE extensions are cross-IDE
197 self
.askyesno
= tkMessageBox
.askyesno
198 self
.askinteger
= tkSimpleDialog
.askinteger
199 self
.showerror
= tkMessageBox
.showerror
201 if self
.extensions
.has_key('AutoIndent'):
202 self
.extensions
['AutoIndent'].set_indentation_params(
203 self
.ispythonsource(filename
))
204 self
.set_status_bar()
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
)
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
)
221 if self
.top
.wm_state() == "iconic":
222 self
.top
.wm_deiconify()
225 self
.text
.focus_set()
230 ("format", "F_ormat"),
232 ("windows", "_Windows"),
236 def createmenubar(self
):
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
)
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")
254 if end
> self
.wmenu_end
:
255 menu
.delete(self
.wmenu_end
+1, end
)
256 WindowList
.add_windows_to_menu(menu
)
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
))
267 iswin
= sys
.platform
[:3] == 'win'
269 self
.text
.config(cursor
="arrow")
270 rmenu
.tk_popup(event
.x_root
, event
.y_root
)
272 self
.text
.config(cursor
="ibeam")
275 # ("Label", "<<virtual-event>>"), ...
276 ("Close", "<<close-window>>"), # Example
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
)
287 def about_dialog(self
, event
=None):
288 tkMessageBox
.showinfo(self
.about_title
, self
.about_text
,
291 helpfile
= "help.txt"
293 def good_advice(self
, event
=None):
294 tkMessageBox
.showinfo('Advice', "Don't Panic!", master
=self
.text
)
296 def help_dialog(self
, event
=None):
298 helpfile
= os
.path
.join(os
.path
.dirname(__file__
), self
.helpfile
)
300 helpfile
= self
.helpfile
302 self
.flist
.open(helpfile
)
304 self
.io
.loadfile(helpfile
)
306 help_viewer
= "netscape -remote 'openurl(%(url)s)' 2>/dev/null || " \
308 help_url
= "http://www.python.org/doc/current/"
310 def python_docs(self
, event
=None):
311 if sys
.platform
=='win32':
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)
320 cmd
= self
.help_viewer
% {"url": self
.help_url
}
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")
329 def remove_selection(self
, event
=None):
330 self
.text
.tag_remove("sel", "1.0", "end")
331 self
.text
.see("insert")
333 def open_module(self
, event
=None):
334 # XXX Shouldn't this be in IOBinding or in FileList?
336 name
= self
.text
.get("sel.first", "sel.last")
340 name
= string
.strip(name
)
342 name
= tkSimpleDialog
.askstring("Module",
343 "Enter the name of a Python module\n"
344 "to search on sys.path and open:",
347 name
= string
.strip(name
)
350 # XXX Ought to support package syntax
351 # XXX Ought to insert current file's directory in front of path
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
)
357 if type != imp
.PY_SOURCE
:
358 tkMessageBox
.showerror("Unsupported type",
359 "%s is not a source module" % name
, parent
=self
.text
)
364 self
.flist
.open(file)
366 self
.io
.loadfile(file)
368 def open_class_browser(self
, event
=None):
369 filename
= self
.io
.filename
371 tkMessageBox
.showerror(
373 "This buffer has no associated filename",
375 self
.text
.focus_set()
377 head
, tail
= os
.path
.split(filename
)
378 base
, ext
= os
.path
.splitext(tail
)
380 ClassBrowser
.ClassBrowser(self
.flist
, base
, [head
])
382 def open_path_browser(self
, event
=None):
384 PathBrowser
.PathBrowser(self
.flist
)
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")
393 def ispythonsource(self
, filename
):
396 base
, ext
= os
.path
.splitext(os
.path
.basename(filename
))
397 if os
.path
.normcase(ext
) in (".py", ".pyw"):
405 return line
[:2] == '#!' and string
.find(line
, 'python') >= 0
407 def close_hook(self
):
409 self
.flist
.close_edit(self
)
411 def set_close_hook(self
, close_hook
):
412 self
.close_hook
= close_hook
414 def filename_change_hook(self
):
416 self
.flist
.filename_changed_edit(self
)
417 self
.saved_change_hook()
418 if self
.ispythonsource(self
.io
.filename
):
423 def addcolorizer(self
):
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
)
432 def rmcolorizer(self
):
435 ##print "Remove colorizer"
436 self
.per
.removefilter(self
.undo
)
437 self
.per
.removefilter(self
.color
)
439 self
.per
.insertfilter(self
.undo
)
441 def saved_change_hook(self
):
442 short
= self
.short_title()
443 long = self
.long_title()
445 title
= short
+ " - " + long
452 icon
= short
or long or title
453 if not self
.get_saved():
454 title
= "*%s*" % title
456 self
.top
.wm_title(title
)
457 self
.top
.wm_iconname(icon
)
460 return self
.undo
.get_saved()
462 def set_saved(self
, flag
):
463 self
.undo
.set_saved(flag
)
465 def reset_undo(self
):
466 self
.undo
.reset_undo()
468 def short_title(self
):
469 filename
= self
.io
.filename
471 filename
= os
.path
.basename(filename
)
474 def long_title(self
):
475 return self
.io
.filename
or ""
477 def center_insert_event(self
, event
):
480 def center(self
, mark
="insert"):
482 top
, bot
= self
.getwindowlines()
483 lineno
= self
.getlineno(mark
)
485 newtop
= max(1, lineno
- height
/2)
486 text
.yview(float(newtop
))
488 def getwindowlines(self
):
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
498 def getlineno(self
, mark
="insert"):
500 return int(float(text
.index(mark
)))
502 def close_event(self
, event
):
507 return self
.io
.maybesave()
510 self
.top
.wm_deiconify()
512 reply
= self
.maybesave()
513 if reply
!= "cancel":
518 WindowList
.unregister_callback(self
.postwindowsmenu
)
523 self
.unload_extensions()
524 self
.io
.close(); self
.io
= None
525 self
.undo
= None # XXX
527 colorizing
= self
.color
.colorizing
528 doh
= colorizing
and self
.top
529 self
.color
.close(doh
) # Cancel colorization
532 self
.per
.close(); self
.per
= None
536 def load_extensions(self
):
538 self
.load_standard_extensions()
540 def unload_extensions(self
):
541 for ins
in self
.extensions
.values():
542 if hasattr(ins
, "close"):
546 def load_standard_extensions(self
):
547 for name
in self
.get_standard_extension_names():
549 self
.load_extension(name
)
551 print "Failed to load extension", `name`
553 traceback
.print_exc()
555 def get_standard_extension_names(self
):
556 return idleconf
.getextensions()
558 def load_extension(self
, name
):
559 mod
= __import__(name
, globals(), locals(), [])
560 cls
= getattr(mod
, name
)
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")
569 kdnames
.append("unix_keydefs")
571 for kdname
in kdnames
:
572 if hasattr(ins
, kdname
):
573 keydefs
.update(getattr(ins
, kdname
))
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
)
589 def apply_bindings(self
, keydefs
=None):
591 keydefs
= self
.Bindings
.default_keydefs
593 text
.keydefs
= keydefs
594 for event
, keylist
in keydefs
.items():
596 apply(text
.event_add
, (event
,) + tuple(keylist
))
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.
602 defs
= self
.Bindings
.menudefs
604 keydefs
= self
.Bindings
.default_keydefs
605 menudict
= self
.menudict
607 for mname
, itemlist
in defs
:
608 menu
= menudict
.get(mname
)
611 for item
in itemlist
:
616 checkbutton
= (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
)
624 var
= self
.getrawvar(event
, BooleanVar
)
625 menu
.add_checkbutton(label
=label
, underline
=underline
,
626 command
=command
, accelerator
=accelerator
,
629 menu
.add_command(label
=label
, underline
=underline
,
630 command
=command
, accelerator
=accelerator
)
632 def getvar(self
, name
):
633 var
= self
.getrawvar(name
)
637 def setvar(self
, name
, value
, vartype
=None):
638 var
= self
.getrawvar(name
, vartype
)
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
)
648 # Tk implementations of "virtual text methods" -- each platform
649 # reusing IDLE's support code needs to define these for its GUI's
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.
657 def is_char_in_string(self
, text_index
):
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
)
664 # The colorizer is missing: assume the worst
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
):
671 first
= self
.text
.index("sel.first")
672 last
= self
.text
.index("sel.last")
677 # Return the text widget's current view of what a tab stop means
678 # (equivalent width in spaces).
680 def get_tabwidth(self
):
681 current
= self
.text
['tabs'] or TK_TABWIDTH_DEFAULT
684 # Set the text widget's current view of what a tab stop means.
686 def set_tabwidth(self
, newtabwidth
):
688 if self
.get_tabwidth() != newtabwidth
:
689 pixels
= text
.tk
.call("font", "measure", text
["font"],
690 "-displayof", text
.master
,
692 text
.configure(tabs
=pixels
)
695 # Helper to extract the underscore from a string, e.g.
696 # prepstr("Co_py") returns (2, "Copy").
697 i
= string
.find(s
, '_')
709 def get_accelerator(keydefs
, event
):
710 keylist
= keydefs
.get(event
)
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
)
726 def 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)
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_]')
740 filename
= sys
.argv
[1]
743 edit
= EditorWindow(root
=root
, filename
=filename
)
744 edit
.set_close_hook(root
.quit
)
748 if __name__
== '__main__':