12 from IdleConf
import idleconf
14 # The default tab setting for a Text widget, in average-width characters.
15 TK_TABWIDTH_DEFAULT
= 8
19 #$ event <<open-module>>
21 #$ unix <Control-x><Control-m>
23 #$ event <<open-class-browser>>
25 #$ unix <Control-x><Control-b>
27 #$ event <<open-path-browser>>
29 #$ event <<close-window>>
31 #$ unix <Control-x><Control-0>
32 #$ unix <Control-x><Key-0>
49 #$ event <<select-all>>
59 #$ event <<about-idle>>
61 # Events without menu entries
63 #$ event <<remove-selection>>
66 #$ event <<center-insert>>
70 #$ event <<do-nothing>>
74 about_title
= "About IDLE"
78 An Integrated DeveLopment Environment for Python
81 """ % idlever
.IDLE_VERSION
83 def _find_module(fullname
, path
=None):
84 """Version of imp.find_module() that handles hierarchical module names"""
87 for tgt
in fullname
.split('.'):
89 file.close() # close intermediate files
90 (file, filename
, descr
) = imp
.find_module(tgt
, path
)
91 if descr
[2] == imp
.PY_SOURCE
:
92 break # find but not load the source file
93 module
= imp
.load_module(tgt
, file, filename
, descr
)
95 path
= module
.__path
__
96 except AttributeError:
97 raise ImportError, 'No source for module ' + module
.__name
__
98 return file, filename
, descr
102 from Percolator
import Percolator
103 from ColorDelegator
import ColorDelegator
104 from UndoDelegator
import UndoDelegator
105 from IOBinding
import IOBinding
107 from Tkinter
import Toplevel
108 from MultiStatusBar
import MultiStatusBar
110 about_title
= about_title
111 about_text
= about_text
114 runnable
= False # Shell window cannot Import Module or Run Script
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("<<python-docs>>", self
.python_docs
)
146 text
.bind("<<about-idle>>", self
.about_dialog
)
147 text
.bind("<<open-module>>", self
.open_module
)
148 text
.bind("<<do-nothing>>", lambda event
: "break")
149 text
.bind("<<select-all>>", self
.select_all
)
150 text
.bind("<<remove-selection>>", self
.remove_selection
)
151 text
.bind("<3>", self
.right_menu_event
)
153 flist
.inversedict
[self
] = key
155 flist
.dict[key
] = self
156 text
.bind("<<open-new-window>>", self
.flist
.new_callback
)
157 text
.bind("<<close-all-windows>>", self
.flist
.close_all_callback
)
158 text
.bind("<<open-class-browser>>", self
.open_class_browser
)
159 text
.bind("<<open-path-browser>>", self
.open_path_browser
)
161 vbar
['command'] = text
.yview
162 vbar
.pack(side
=RIGHT
, fill
=Y
)
164 text
['yscrollcommand'] = vbar
.set
165 text
['font'] = edconf
.get('font-name'), edconf
.get('font-size')
166 text_frame
.pack(side
=LEFT
, fill
=BOTH
, expand
=1)
167 text
.pack(side
=TOP
, fill
=BOTH
, expand
=1)
170 self
.per
= per
= self
.Percolator(text
)
171 if self
.ispythonsource(filename
):
172 self
.color
= color
= self
.ColorDelegator(); per
.insertfilter(color
)
173 ##print "Initial colorizer"
175 ##print "No initial colorizer"
177 self
.undo
= undo
= self
.UndoDelegator(); per
.insertfilter(undo
)
178 self
.io
= io
= self
.IOBinding(self
)
180 text
.undo_block_start
= undo
.undo_block_start
181 text
.undo_block_stop
= undo
.undo_block_stop
182 undo
.set_saved_change_hook(self
.saved_change_hook
)
183 io
.set_filename_change_hook(self
.filename_change_hook
)
186 if os
.path
.exists(filename
):
187 io
.loadfile(filename
)
189 io
.set_filename(filename
)
191 self
.saved_change_hook()
193 self
.load_extensions()
195 menu
= self
.menudict
.get('windows')
197 end
= menu
.index("end")
204 WindowList
.register_callback(self
.postwindowsmenu
)
206 # Some abstractions so IDLE extensions are cross-IDE
207 self
.askyesno
= tkMessageBox
.askyesno
208 self
.askinteger
= tkSimpleDialog
.askinteger
209 self
.showerror
= tkMessageBox
.showerror
211 if self
.extensions
.has_key('AutoIndent'):
212 self
.extensions
['AutoIndent'].set_indentation_params(
213 self
.ispythonsource(filename
))
214 self
.set_status_bar()
216 def set_status_bar(self
):
217 self
.status_bar
= self
.MultiStatusBar(self
.text_frame
)
218 self
.status_bar
.set_label('column', 'Col: ?', side
=RIGHT
)
219 self
.status_bar
.set_label('line', 'Ln: ?', side
=RIGHT
)
220 self
.status_bar
.pack(side
=BOTTOM
, fill
=X
)
221 self
.text
.bind('<KeyRelease>', self
.set_line_and_column
)
222 self
.text
.bind('<ButtonRelease>', self
.set_line_and_column
)
223 self
.text
.after_idle(self
.set_line_and_column
)
225 def set_line_and_column(self
, event
=None):
226 line
, column
= self
.text
.index(INSERT
).split('.')
227 self
.status_bar
.set_label('column', 'Col: %s' % column
)
228 self
.status_bar
.set_label('line', 'Ln: %s' % line
)
231 if self
.top
.wm_state() == "iconic":
232 self
.top
.wm_deiconify()
235 self
.text
.focus_set()
240 ("windows", "_Windows"),
244 def createmenubar(self
):
246 self
.menudict
= menudict
= {}
247 for name
, label
in self
.menu_specs
:
248 underline
, label
= prepstr(label
)
249 menudict
[name
] = menu
= Menu(mbar
, name
=name
)
250 mbar
.add_cascade(label
=label
, menu
=menu
, underline
=underline
)
253 def postwindowsmenu(self
):
254 # Only called when Windows menu exists
255 # XXX Actually, this Just-In-Time updating interferes badly
256 # XXX with the tear-off feature. It would be better to update
257 # XXX all Windows menus whenever the list of windows changes.
258 menu
= self
.menudict
['windows']
259 end
= menu
.index("end")
262 if end
> self
.wmenu_end
:
263 menu
.delete(self
.wmenu_end
+1, end
)
264 WindowList
.add_windows_to_menu(menu
)
268 def right_menu_event(self
, event
):
269 self
.text
.tag_remove("sel", "1.0", "end")
270 self
.text
.mark_set("insert", "@%d,%d" % (event
.x
, event
.y
))
275 iswin
= sys
.platform
[:3] == 'win'
277 self
.text
.config(cursor
="arrow")
278 rmenu
.tk_popup(event
.x_root
, event
.y_root
)
280 self
.text
.config(cursor
="ibeam")
283 # ("Label", "<<virtual-event>>"), ...
284 ("Close", "<<close-window>>"), # Example
287 def make_rmenu(self
):
288 rmenu
= Menu(self
.text
, tearoff
=0)
289 for label
, eventname
in self
.rmenu_specs
:
290 def command(text
=self
.text
, eventname
=eventname
):
291 text
.event_generate(eventname
)
292 rmenu
.add_command(label
=label
, command
=command
)
295 def about_dialog(self
, event
=None):
296 tkMessageBox
.showinfo(self
.about_title
, self
.about_text
,
299 helpfile
= "help.txt"
301 def help_dialog(self
, event
=None):
303 helpfile
= os
.path
.join(os
.path
.dirname(__file__
), self
.helpfile
)
305 helpfile
= self
.helpfile
307 self
.flist
.open(helpfile
)
309 self
.io
.loadfile(helpfile
)
311 help_url
= "http://www.python.org/doc/current/"
312 if sys
.platform
[:3] == "win":
313 fn
= os
.path
.dirname(__file__
)
314 fn
= os
.path
.join(fn
, os
.pardir
, os
.pardir
, "pythlp.chm")
315 fn
= os
.path
.normpath(fn
)
316 if os
.path
.isfile(fn
):
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 os
.startfile(self
.help_url
)
329 def python_docs(self
, event
=None):
330 webbrowser
.open(self
.help_url
)
332 def select_all(self
, event
=None):
333 self
.text
.tag_add("sel", "1.0", "end-1c")
334 self
.text
.mark_set("insert", "1.0")
335 self
.text
.see("insert")
338 def remove_selection(self
, event
=None):
339 self
.text
.tag_remove("sel", "1.0", "end")
340 self
.text
.see("insert")
342 def open_module(self
, event
=None):
343 # XXX Shouldn't this be in IOBinding or in FileList?
345 name
= self
.text
.get("sel.first", "sel.last")
351 name
= tkSimpleDialog
.askstring("Module",
352 "Enter the name of a Python module\n"
353 "to search on sys.path and open:",
359 # XXX Ought to insert current file's directory in front of path
361 (f
, file, (suffix
, mode
, type)) = _find_module(name
)
362 except (NameError, ImportError), msg
:
363 tkMessageBox
.showerror("Import error", str(msg
), parent
=self
.text
)
365 if type != imp
.PY_SOURCE
:
366 tkMessageBox
.showerror("Unsupported type",
367 "%s is not a source module" % name
, parent
=self
.text
)
372 self
.flist
.open(file)
374 self
.io
.loadfile(file)
376 def open_class_browser(self
, event
=None):
377 filename
= self
.io
.filename
379 tkMessageBox
.showerror(
381 "This buffer has no associated filename",
383 self
.text
.focus_set()
385 head
, tail
= os
.path
.split(filename
)
386 base
, ext
= os
.path
.splitext(tail
)
388 ClassBrowser
.ClassBrowser(self
.flist
, base
, [head
])
390 def open_path_browser(self
, event
=None):
392 PathBrowser
.PathBrowser(self
.flist
)
394 def gotoline(self
, lineno
):
395 if lineno
is not None and lineno
> 0:
396 self
.text
.mark_set("insert", "%d.0" % lineno
)
397 self
.text
.tag_remove("sel", "1.0", "end")
398 self
.text
.tag_add("sel", "insert", "insert +1l")
401 def ispythonsource(self
, filename
):
404 base
, ext
= os
.path
.splitext(os
.path
.basename(filename
))
405 if os
.path
.normcase(ext
) in (".py", ".pyw"):
413 return line
.startswith('#!') and 'python' in line
415 def close_hook(self
):
417 self
.flist
.close_edit(self
)
419 def set_close_hook(self
, close_hook
):
420 self
.close_hook
= close_hook
422 def filename_change_hook(self
):
424 self
.flist
.filename_changed_edit(self
)
425 self
.saved_change_hook()
426 if self
.ispythonsource(self
.io
.filename
):
431 def addcolorizer(self
):
434 ##print "Add colorizer"
435 self
.per
.removefilter(self
.undo
)
436 self
.color
= self
.ColorDelegator()
437 self
.per
.insertfilter(self
.color
)
438 self
.per
.insertfilter(self
.undo
)
440 def rmcolorizer(self
):
443 ##print "Remove colorizer"
444 self
.per
.removefilter(self
.undo
)
445 self
.per
.removefilter(self
.color
)
447 self
.per
.insertfilter(self
.undo
)
449 def saved_change_hook(self
):
450 short
= self
.short_title()
451 long = self
.long_title()
453 title
= short
+ " - " + long
460 icon
= short
or long or title
461 if not self
.get_saved():
462 title
= "*%s*" % title
464 self
.top
.wm_title(title
)
465 self
.top
.wm_iconname(icon
)
468 return self
.undo
.get_saved()
470 def set_saved(self
, flag
):
471 self
.undo
.set_saved(flag
)
473 def reset_undo(self
):
474 self
.undo
.reset_undo()
476 def short_title(self
):
477 filename
= self
.io
.filename
479 filename
= os
.path
.basename(filename
)
482 def long_title(self
):
483 return self
.io
.filename
or ""
485 def center_insert_event(self
, event
):
488 def center(self
, mark
="insert"):
490 top
, bot
= self
.getwindowlines()
491 lineno
= self
.getlineno(mark
)
493 newtop
= max(1, lineno
- height
//2)
494 text
.yview(float(newtop
))
496 def getwindowlines(self
):
498 top
= self
.getlineno("@0,0")
499 bot
= self
.getlineno("@0,65535")
500 if top
== bot
and text
.winfo_height() == 1:
501 # Geometry manager hasn't run yet
502 height
= int(text
['height'])
503 bot
= top
+ height
- 1
506 def getlineno(self
, mark
="insert"):
508 return int(float(text
.index(mark
)))
510 def close_event(self
, event
):
515 return self
.io
.maybesave()
518 self
.top
.wm_deiconify()
520 reply
= self
.maybesave()
521 if reply
!= "cancel":
526 WindowList
.unregister_callback(self
.postwindowsmenu
)
531 self
.unload_extensions()
532 self
.io
.close(); self
.io
= None
533 self
.undo
= None # XXX
535 colorizing
= self
.color
.colorizing
536 doh
= colorizing
and self
.top
537 self
.color
.close(doh
) # Cancel colorization
540 self
.per
.close(); self
.per
= None
544 def load_extensions(self
):
546 self
.load_standard_extensions()
548 def unload_extensions(self
):
549 for ins
in self
.extensions
.values():
550 if hasattr(ins
, "close"):
554 def load_standard_extensions(self
):
555 for name
in self
.get_standard_extension_names():
557 self
.load_extension(name
)
559 print "Failed to load extension", `name`
561 traceback
.print_exc()
563 def get_standard_extension_names(self
):
564 return idleconf
.getextensions()
566 def load_extension(self
, name
):
567 mod
= __import__(name
, globals(), locals(), [])
568 cls
= getattr(mod
, name
)
570 self
.extensions
[name
] = ins
571 kdnames
= ["keydefs"]
572 if sys
.platform
== 'win32':
573 kdnames
.append("windows_keydefs")
574 elif sys
.platform
== 'mac':
575 kdnames
.append("mac_keydefs")
577 kdnames
.append("unix_keydefs")
579 for kdname
in kdnames
:
580 if hasattr(ins
, kdname
):
581 keydefs
.update(getattr(ins
, kdname
))
583 self
.apply_bindings(keydefs
)
584 for vevent
in keydefs
.keys():
585 methodname
= vevent
.replace("-", "_")
586 while methodname
[:1] == '<':
587 methodname
= methodname
[1:]
588 while methodname
[-1:] == '>':
589 methodname
= methodname
[:-1]
590 methodname
= methodname
+ "_event"
591 if hasattr(ins
, methodname
):
592 self
.text
.bind(vevent
, getattr(ins
, methodname
))
593 if hasattr(ins
, "menudefs"):
594 self
.fill_menus(ins
.menudefs
, keydefs
)
597 def apply_bindings(self
, keydefs
=None):
599 keydefs
= self
.Bindings
.default_keydefs
601 text
.keydefs
= keydefs
602 for event
, keylist
in keydefs
.items():
604 apply(text
.event_add
, (event
,) + tuple(keylist
))
606 def fill_menus(self
, defs
=None, keydefs
=None):
607 # Fill the menus. Menus that are absent or None in
608 # self.menudict are ignored.
610 defs
= self
.Bindings
.menudefs
612 keydefs
= self
.Bindings
.default_keydefs
613 menudict
= self
.menudict
615 for mname
, itemlist
in defs
:
616 menu
= menudict
.get(mname
)
619 for item
in itemlist
:
624 checkbutton
= (label
[:1] == '!')
627 underline
, label
= prepstr(label
)
628 accelerator
= get_accelerator(keydefs
, event
)
629 def command(text
=text
, event
=event
):
630 text
.event_generate(event
)
632 var
= self
.getrawvar(event
, BooleanVar
)
633 menu
.add_checkbutton(label
=label
, underline
=underline
,
634 command
=command
, accelerator
=accelerator
,
637 menu
.add_command(label
=label
, underline
=underline
,
638 command
=command
, accelerator
=accelerator
)
640 def getvar(self
, name
):
641 var
= self
.getrawvar(name
)
645 def setvar(self
, name
, value
, vartype
=None):
646 var
= self
.getrawvar(name
, vartype
)
650 def getrawvar(self
, name
, vartype
=None):
651 var
= self
.vars.get(name
)
652 if not var
and vartype
:
653 self
.vars[name
] = var
= vartype(self
.text
)
656 # Tk implementations of "virtual text methods" -- each platform
657 # reusing IDLE's support code needs to define these for its GUI's
660 # Is character at text_index in a Python string? Return 0 for
661 # "guaranteed no", true for anything else. This info is expensive
662 # to compute ab initio, but is probably already known by the
663 # platform's colorizer.
665 def is_char_in_string(self
, text_index
):
667 # Return true iff colorizer hasn't (re)gotten this far
668 # yet, or the character is tagged as being in a string
669 return self
.text
.tag_prevrange("TODO", text_index
) or \
670 "STRING" in self
.text
.tag_names(text_index
)
672 # The colorizer is missing: assume the worst
675 # If a selection is defined in the text widget, return (start,
676 # end) as Tkinter text indices, otherwise return (None, None)
677 def get_selection_indices(self
):
679 first
= self
.text
.index("sel.first")
680 last
= self
.text
.index("sel.last")
685 # Return the text widget's current view of what a tab stop means
686 # (equivalent width in spaces).
688 def get_tabwidth(self
):
689 current
= self
.text
['tabs'] or TK_TABWIDTH_DEFAULT
692 # Set the text widget's current view of what a tab stop means.
694 def set_tabwidth(self
, newtabwidth
):
696 if self
.get_tabwidth() != newtabwidth
:
697 pixels
= text
.tk
.call("font", "measure", text
["font"],
698 "-displayof", text
.master
,
700 text
.configure(tabs
=pixels
)
703 # Helper to extract the underscore from a string, e.g.
704 # prepstr("Co_py") returns (2, "Copy").
717 def get_accelerator(keydefs
, event
):
718 keylist
= keydefs
.get(event
)
722 s
= re
.sub(r
"-[a-z]\b", lambda m
: m
.group().upper(), s
)
723 s
= re
.sub(r
"\b\w+\b", lambda m
: keynames
.get(m
.group(), m
.group()), s
)
724 s
= re
.sub("Key-", "", s
)
725 s
= re
.sub("Control-", "Ctrl-", s
)
726 s
= re
.sub("-", "+", s
)
727 s
= re
.sub("><", " ", s
)
728 s
= re
.sub("<", "", s
)
729 s
= re
.sub(">", "", s
)
733 def fixwordbreaks(root
):
734 # Make sure that Tk's double-click and next/previous word
735 # operations use our definition of a word (i.e. an identifier)
737 tk
.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
738 tk
.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
739 tk
.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
747 filename
= sys
.argv
[1]
750 edit
= EditorWindow(root
=root
, filename
=filename
)
751 edit
.set_close_hook(root
.quit
)
755 if __name__
== '__main__':