12 # The default tab setting for a Text widget, in average-width characters.
13 TK_TABWIDTH_DEFAULT
= 8
17 #$ event <<open-module>>
19 #$ unix <Control-x><Control-m>
21 #$ event <<open-class-browser>>
23 #$ unix <Control-x><Control-b>
25 #$ event <<open-path-browser>>
27 #$ event <<close-window>>
28 #$ unix <Control-x><Control-0>
29 #$ unix <Control-x><Key-0>
46 #$ event <<select-all>>
56 #$ event <<about-idle>>
58 # Events without menu entries
60 #$ event <<remove-selection>>
63 #$ event <<center-insert>>
67 #$ event <<do-nothing>>
71 about_title
= "About IDLE"
75 An Integrated DeveLopment Environment for Python
78 """ % idlever
.IDLE_VERSION
82 from Percolator
import Percolator
83 from ColorDelegator
import ColorDelegator
84 from UndoDelegator
import UndoDelegator
85 from IOBinding
import IOBinding
87 from Tkinter
import Toplevel
89 about_title
= about_title
90 about_text
= about_text
94 def __init__(self
, flist
=None, filename
=None, key
=None, root
=None):
95 cprefs
= self
.ColorDelegator
.cprefs
97 root
= root
or flist
.root
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
= text
= Text(top
, name
='text', padx
=5,
105 foreground
=cprefs
.CNormal
[0],
106 background
=cprefs
.CNormal
[1],
107 highlightcolor
=cprefs
.CHilite
[0],
108 highlightbackground
=cprefs
.CHilite
[1],
109 insertbackground
=cprefs
.CCursor
[1],
113 self
.apply_bindings()
115 self
.top
.protocol("WM_DELETE_WINDOW", self
.close
)
116 self
.top
.bind("<<close-window>>", self
.close_event
)
117 text
.bind("<<center-insert>>", self
.center_insert_event
)
118 text
.bind("<<help>>", self
.help_dialog
)
119 text
.bind("<<python-docs>>", self
.python_docs
)
120 text
.bind("<<about-idle>>", self
.about_dialog
)
121 text
.bind("<<open-module>>", self
.open_module
)
122 text
.bind("<<do-nothing>>", lambda event
: "break")
123 text
.bind("<<select-all>>", self
.select_all
)
124 text
.bind("<<remove-selection>>", self
.remove_selection
)
125 text
.bind("<3>", self
.right_menu_event
)
127 flist
.inversedict
[self
] = key
129 flist
.dict[key
] = self
130 text
.bind("<<open-new-window>>", self
.flist
.new_callback
)
131 text
.bind("<<close-all-windows>>", self
.flist
.close_all_callback
)
132 text
.bind("<<open-class-browser>>", self
.open_class_browser
)
133 text
.bind("<<open-path-browser>>", self
.open_path_browser
)
135 vbar
['command'] = text
.yview
136 vbar
.pack(side
=RIGHT
, fill
=Y
)
138 text
['yscrollcommand'] = vbar
.set
139 if sys
.platform
[:3] == 'win':
140 text
['font'] = ("lucida console", 8)
141 # text['font'] = ("courier new", 10)
142 text
.pack(side
=LEFT
, fill
=BOTH
, expand
=1)
145 self
.per
= per
= self
.Percolator(text
)
146 if self
.ispythonsource(filename
):
147 self
.color
= color
= self
.ColorDelegator(); per
.insertfilter(color
)
148 ##print "Initial colorizer"
150 ##print "No initial colorizer"
152 self
.undo
= undo
= self
.UndoDelegator(); per
.insertfilter(undo
)
153 self
.io
= io
= self
.IOBinding(self
)
155 text
.undo_block_start
= undo
.undo_block_start
156 text
.undo_block_stop
= undo
.undo_block_stop
157 undo
.set_saved_change_hook(self
.saved_change_hook
)
158 io
.set_filename_change_hook(self
.filename_change_hook
)
161 if os
.path
.exists(filename
):
162 io
.loadfile(filename
)
164 io
.set_filename(filename
)
166 self
.saved_change_hook()
168 self
.load_extensions()
170 menu
= self
.menudict
.get('windows')
172 end
= menu
.index("end")
179 WindowList
.register_callback(self
.postwindowsmenu
)
181 # Some abstractions so IDLE extensions are cross-IDE
182 self
.askyesno
= tkMessageBox
.askyesno
183 self
.askinteger
= tkSimpleDialog
.askinteger
184 self
.showerror
= tkMessageBox
.showerror
186 if self
.extensions
.has_key('AutoIndent'):
187 self
.extensions
['AutoIndent'].set_indentation_params(
188 self
.ispythonsource(filename
))
191 if self
.top
.wm_state() == "iconic":
192 self
.top
.wm_deiconify()
195 self
.text
.focus_set()
200 ("windows", "_Windows"),
204 def createmenubar(self
):
206 self
.menudict
= menudict
= {}
207 for name
, label
in self
.menu_specs
:
208 underline
, label
= prepstr(label
)
209 menudict
[name
] = menu
= Menu(mbar
, name
=name
)
210 mbar
.add_cascade(label
=label
, menu
=menu
, underline
=underline
)
213 def postwindowsmenu(self
):
214 # Only called when Windows menu exists
215 # XXX Actually, this Just-In-Time updating interferes badly
216 # XXX with the tear-off feature. It would be better to update
217 # XXX all Windows menus whenever the list of windows changes.
218 menu
= self
.menudict
['windows']
219 end
= menu
.index("end")
222 if end
> self
.wmenu_end
:
223 menu
.delete(self
.wmenu_end
+1, end
)
224 WindowList
.add_windows_to_menu(menu
)
228 def right_menu_event(self
, event
):
229 self
.text
.tag_remove("sel", "1.0", "end")
230 self
.text
.mark_set("insert", "@%d,%d" % (event
.x
, event
.y
))
235 iswin
= sys
.platform
[:3] == 'win'
237 self
.text
.config(cursor
="arrow")
238 rmenu
.tk_popup(event
.x_root
, event
.y_root
)
240 self
.text
.config(cursor
="ibeam")
243 # ("Label", "<<virtual-event>>"), ...
244 ("Close", "<<close-window>>"), # Example
247 def make_rmenu(self
):
248 rmenu
= Menu(self
.text
, tearoff
=0)
249 for label
, eventname
in self
.rmenu_specs
:
250 def command(text
=self
.text
, eventname
=eventname
):
251 text
.event_generate(eventname
)
252 rmenu
.add_command(label
=label
, command
=command
)
255 def about_dialog(self
, event
=None):
256 tkMessageBox
.showinfo(self
.about_title
, self
.about_text
,
259 helpfile
= "help.txt"
261 def help_dialog(self
, event
=None):
263 helpfile
= os
.path
.join(os
.path
.dirname(__file__
), self
.helpfile
)
265 helpfile
= self
.helpfile
267 self
.flist
.open(helpfile
)
269 self
.io
.loadfile(helpfile
)
271 # XXX Fix these for Windows
272 help_viewer
= "netscape -remote 'openurl(%(url)s)' 2>/dev/null || " \
274 help_url
= "http://www.python.org/doc/current/"
276 def python_docs(self
, event
=None):
277 cmd
= self
.help_viewer
% {"url": self
.help_url
}
280 def select_all(self
, event
=None):
281 self
.text
.tag_add("sel", "1.0", "end-1c")
282 self
.text
.mark_set("insert", "1.0")
283 self
.text
.see("insert")
286 def remove_selection(self
, event
=None):
287 self
.text
.tag_remove("sel", "1.0", "end")
288 self
.text
.see("insert")
290 def open_module(self
, event
=None):
291 # XXX Shouldn't this be in IOBinding or in FileList?
293 name
= self
.text
.get("sel.first", "sel.last")
297 name
= string
.strip(name
)
299 name
= tkSimpleDialog
.askstring("Module",
300 "Enter the name of a Python module\n"
301 "to search on sys.path and open:",
304 name
= string
.strip(name
)
307 # XXX Ought to support package syntax
308 # XXX Ought to insert current file's directory in front of path
310 (f
, file, (suffix
, mode
, type)) = imp
.find_module(name
)
311 except (NameError, ImportError), msg
:
312 tkMessageBox
.showerror("Import error", str(msg
), parent
=self
.text
)
314 if type != imp
.PY_SOURCE
:
315 tkMessageBox
.showerror("Unsupported type",
316 "%s is not a source module" % name
, parent
=self
.text
)
321 self
.flist
.open(file)
323 self
.io
.loadfile(file)
325 def open_class_browser(self
, event
=None):
326 filename
= self
.io
.filename
328 tkMessageBox
.showerror(
330 "This buffer has no associated filename",
332 self
.text
.focus_set()
334 head
, tail
= os
.path
.split(filename
)
335 base
, ext
= os
.path
.splitext(tail
)
337 ClassBrowser
.ClassBrowser(self
.flist
, base
, [head
])
339 def open_path_browser(self
, event
=None):
341 PathBrowser
.PathBrowser(self
.flist
)
343 def gotoline(self
, lineno
):
344 if lineno
is not None and lineno
> 0:
345 self
.text
.mark_set("insert", "%d.0" % lineno
)
346 self
.text
.tag_remove("sel", "1.0", "end")
347 self
.text
.tag_add("sel", "insert", "insert +1l")
350 def ispythonsource(self
, filename
):
353 base
, ext
= os
.path
.splitext(os
.path
.basename(filename
))
354 if os
.path
.normcase(ext
) in (".py", ".pyw"):
362 return line
[:2] == '#!' and string
.find(line
, 'python') >= 0
364 def close_hook(self
):
366 self
.flist
.close_edit(self
)
368 def set_close_hook(self
, close_hook
):
369 self
.close_hook
= close_hook
371 def filename_change_hook(self
):
373 self
.flist
.filename_changed_edit(self
)
374 self
.saved_change_hook()
375 if self
.ispythonsource(self
.io
.filename
):
380 def addcolorizer(self
):
383 ##print "Add colorizer"
384 self
.per
.removefilter(self
.undo
)
385 self
.color
= self
.ColorDelegator()
386 self
.per
.insertfilter(self
.color
)
387 self
.per
.insertfilter(self
.undo
)
389 def rmcolorizer(self
):
392 ##print "Remove colorizer"
393 self
.per
.removefilter(self
.undo
)
394 self
.per
.removefilter(self
.color
)
396 self
.per
.insertfilter(self
.undo
)
398 def saved_change_hook(self
):
399 short
= self
.short_title()
400 long = self
.long_title()
402 title
= short
+ " - " + long
409 icon
= short
or long or title
410 if not self
.get_saved():
411 title
= "*%s*" % title
413 self
.top
.wm_title(title
)
414 self
.top
.wm_iconname(icon
)
417 return self
.undo
.get_saved()
419 def set_saved(self
, flag
):
420 self
.undo
.set_saved(flag
)
422 def reset_undo(self
):
423 self
.undo
.reset_undo()
425 def short_title(self
):
426 filename
= self
.io
.filename
428 filename
= os
.path
.basename(filename
)
431 def long_title(self
):
432 return self
.io
.filename
or ""
434 def center_insert_event(self
, event
):
437 def center(self
, mark
="insert"):
439 top
, bot
= self
.getwindowlines()
440 lineno
= self
.getlineno(mark
)
442 newtop
= max(1, lineno
- height
/2)
443 text
.yview(float(newtop
))
445 def getwindowlines(self
):
447 top
= self
.getlineno("@0,0")
448 bot
= self
.getlineno("@0,65535")
449 if top
== bot
and text
.winfo_height() == 1:
450 # Geometry manager hasn't run yet
451 height
= int(text
['height'])
452 bot
= top
+ height
- 1
455 def getlineno(self
, mark
="insert"):
457 return int(float(text
.index(mark
)))
459 def close_event(self
, event
):
464 return self
.io
.maybesave()
467 self
.top
.wm_deiconify()
469 reply
= self
.maybesave()
470 if reply
!= "cancel":
475 WindowList
.unregister_callback(self
.postwindowsmenu
)
480 self
.unload_extensions()
481 self
.io
.close(); self
.io
= None
482 self
.undo
= None # XXX
484 colorizing
= self
.color
.colorizing
485 doh
= colorizing
and self
.top
486 self
.color
.close(doh
) # Cancel colorization
489 self
.per
.close(); self
.per
= None
493 def load_extensions(self
):
495 self
.load_standard_extensions()
497 def unload_extensions(self
):
498 for ins
in self
.extensions
.values():
499 if hasattr(ins
, "close"):
503 def load_standard_extensions(self
):
504 for name
in self
.get_standard_extension_names():
506 self
.load_extension(name
)
508 print "Failed to load extension", `name`
510 traceback
.print_exc()
512 def get_standard_extension_names(self
):
514 return extend
.standard
516 def load_extension(self
, name
):
517 mod
= __import__(name
, globals(), locals(), [])
518 cls
= getattr(mod
, name
)
520 self
.extensions
[name
] = ins
521 kdnames
= ["keydefs"]
522 if sys
.platform
== 'win32':
523 kdnames
.append("windows_keydefs")
524 elif sys
.platform
== 'mac':
525 kdnames
.append("mac_keydefs")
527 kdnames
.append("unix_keydefs")
529 for kdname
in kdnames
:
530 if hasattr(ins
, kdname
):
531 keydefs
.update(getattr(ins
, kdname
))
533 self
.apply_bindings(keydefs
)
534 for vevent
in keydefs
.keys():
535 methodname
= string
.replace(vevent
, "-", "_")
536 while methodname
[:1] == '<':
537 methodname
= methodname
[1:]
538 while methodname
[-1:] == '>':
539 methodname
= methodname
[:-1]
540 methodname
= methodname
+ "_event"
541 if hasattr(ins
, methodname
):
542 self
.text
.bind(vevent
, getattr(ins
, methodname
))
543 if hasattr(ins
, "menudefs"):
544 self
.fill_menus(ins
.menudefs
, keydefs
)
547 def apply_bindings(self
, keydefs
=None):
549 keydefs
= self
.Bindings
.default_keydefs
551 text
.keydefs
= keydefs
552 for event
, keylist
in keydefs
.items():
554 apply(text
.event_add
, (event
,) + tuple(keylist
))
556 def fill_menus(self
, defs
=None, keydefs
=None):
557 # Fill the menus. Menus that are absent or None in
558 # self.menudict are ignored.
560 defs
= self
.Bindings
.menudefs
562 keydefs
= self
.Bindings
.default_keydefs
563 menudict
= self
.menudict
565 for mname
, itemlist
in defs
:
566 menu
= menudict
.get(mname
)
569 for item
in itemlist
:
574 checkbutton
= (label
[:1] == '!')
577 underline
, label
= prepstr(label
)
578 accelerator
= get_accelerator(keydefs
, event
)
579 def command(text
=text
, event
=event
):
580 text
.event_generate(event
)
582 var
= self
.getrawvar(event
, BooleanVar
)
583 menu
.add_checkbutton(label
=label
, underline
=underline
,
584 command
=command
, accelerator
=accelerator
,
587 menu
.add_command(label
=label
, underline
=underline
,
588 command
=command
, accelerator
=accelerator
)
590 def getvar(self
, name
):
591 var
= self
.getrawvar(name
)
595 def setvar(self
, name
, value
, vartype
=None):
596 var
= self
.getrawvar(name
, vartype
)
600 def getrawvar(self
, name
, vartype
=None):
601 var
= self
.vars.get(name
)
602 if not var
and vartype
:
603 self
.vars[name
] = var
= vartype(self
.text
)
606 # Tk implementations of "virtual text methods" -- each platform
607 # reusing IDLE's support code needs to define these for its GUI's
610 # Is character at text_index in a Python string? Return 0 for
611 # "guaranteed no", true for anything else. This info is expensive
612 # to compute ab initio, but is probably already known by the
613 # platform's colorizer.
615 def is_char_in_string(self
, text_index
):
617 # Return true iff colorizer hasn't (re)gotten this far
618 # yet, or the character is tagged as being in a string
619 return self
.text
.tag_prevrange("TODO", text_index
) or \
620 "STRING" in self
.text
.tag_names(text_index
)
622 # The colorizer is missing: assume the worst
625 # If a selection is defined in the text widget, return (start,
626 # end) as Tkinter text indices, otherwise return (None, None)
627 def get_selection_indices(self
):
629 first
= self
.text
.index("sel.first")
630 last
= self
.text
.index("sel.last")
635 # Return the text widget's current view of what a tab stop means
636 # (equivalent width in spaces).
638 def get_tabwidth(self
):
639 current
= self
.text
['tabs'] or TK_TABWIDTH_DEFAULT
642 # Set the text widget's current view of what a tab stop means.
644 def set_tabwidth(self
, newtabwidth
):
646 if self
.get_tabwidth() != newtabwidth
:
647 pixels
= text
.tk
.call("font", "measure", text
["font"],
648 "-displayof", text
.master
,
650 text
.configure(tabs
=pixels
)
653 # Helper to extract the underscore from a string, e.g.
654 # prepstr("Co_py") returns (2, "Copy").
655 i
= string
.find(s
, '_')
667 def get_accelerator(keydefs
, event
):
668 keylist
= keydefs
.get(event
)
672 s
= re
.sub(r
"-[a-z]\b", lambda m
: string
.upper(m
.group()), s
)
673 s
= re
.sub(r
"\b\w+\b", lambda m
: keynames
.get(m
.group(), m
.group()), s
)
674 s
= re
.sub("Key-", "", s
)
675 s
= re
.sub("Control-", "Ctrl-", s
)
676 s
= re
.sub("-", "+", s
)
677 s
= re
.sub("><", " ", s
)
678 s
= re
.sub("<", "", s
)
679 s
= re
.sub(">", "", s
)
683 def fixwordbreaks(root
):
684 # Make sure that Tk's double-click and next/previous word
685 # operations use our definition of a word (i.e. an identifier)
687 tk
.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
688 tk
.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
689 tk
.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
697 filename
= sys
.argv
[1]
700 edit
= EditorWindow(root
=root
, filename
=filename
)
701 edit
.set_close_hook(root
.quit
)
705 if __name__
== '__main__':