10 from idlelib.MultiCall import MultiCallCreator
11 from idlelib import idlever
12 from idlelib import WindowList
13 from idlelib import SearchDialog
14 from idlelib import GrepDialog
15 from idlelib import ReplaceDialog
16 from idlelib import PyParse
17 from idlelib.configHandler import idleConf
18 from idlelib import aboutDialog, textView, configDialog
19 from idlelib import macosxSupport
21 # The default tab setting for a Text widget, in average-width characters.
22 TK_TABWIDTH_DEFAULT = 8
24 def _sphinx_version():
25 "Format sys.version_info to produce the Sphinx version string used to install the chm docs"
26 major, minor, micro, level, serial = sys.version_info
27 release = '%s%s' % (major, minor)
29 release += '%s' % (micro,)
30 if level == 'candidate':
31 release += 'rc%s' % (serial,)
32 elif level != 'final':
33 release += '%s%s' % (level[0], serial)
36 def _find_module(fullname, path=None):
37 """Version of imp.find_module() that handles hierarchical module names"""
40 for tgt in fullname.split('.'):
42 file.close() # close intermediate files
43 (file, filename, descr) = imp.find_module(tgt, path)
44 if descr[2] == imp.PY_SOURCE:
45 break # find but not load the source file
46 module = imp.load_module(tgt, file, filename, descr)
48 path = module.__path__
49 except AttributeError:
50 raise ImportError, 'No source for module ' + module.__name__
51 if descr[2] != imp.PY_SOURCE:
52 # If all of the above fails and didn't raise an exception,fallback
53 # to a straight import which can find __init__.py in a package.
54 m = __import__(fullname)
57 except AttributeError:
61 base, ext = os.path.splitext(filename)
65 descr = filename, None, imp.PY_SOURCE
66 return file, filename, descr
69 class HelpDialog(object):
72 self.parent = None # parent of help window
73 self.dlg = None # the help window iteself
75 def display(self, parent, near=None):
76 """ Display the help dialog.
78 parent - parent widget for the help window
80 near - a Toplevel widget (e.g. EditorWindow or PyShell)
81 to use as a reference for placing the help window
84 self.show_dialog(parent)
88 def show_dialog(self, parent):
90 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
91 self.dlg = dlg = textView.view_file(parent,'Help',fn, modal=False)
92 dlg.bind('<Destroy>', self.destroy, '+')
94 def nearwindow(self, near):
95 # Place the help dialog near the window specified by parent.
96 # Note - this may not reposition the window in Metacity
97 # if "/apps/metacity/general/disable_workarounds" is enabled
99 geom = (near.winfo_rootx() + 10, near.winfo_rooty() + 10)
101 dlg.geometry("=+%d+%d" % geom)
105 def destroy(self, ev=None):
109 helpDialog = HelpDialog() # singleton instance
112 class EditorWindow(object):
113 from idlelib.Percolator import Percolator
114 from idlelib.ColorDelegator import ColorDelegator
115 from idlelib.UndoDelegator import UndoDelegator
116 from idlelib.IOBinding import IOBinding, filesystemencoding, encoding
117 from idlelib import Bindings
118 from Tkinter import Toplevel
119 from idlelib.MultiStatusBar import MultiStatusBar
123 def __init__(self, flist=None, filename=None, key=None, root=None):
124 if EditorWindow.help_url is None:
125 dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
126 if sys.platform.count('linux'):
127 # look for html docs in a couple of standard places
128 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
129 if os.path.isdir('/var/www/html/python/'): # "python2" rpm
130 dochome = '/var/www/html/python/index.html'
132 basepath = '/usr/share/doc/' # standard location
133 dochome = os.path.join(basepath, pyver,
135 elif sys.platform[:3] == 'win':
136 chmfile = os.path.join(sys.prefix, 'Doc',
137 'Python%s.chm' % _sphinx_version())
138 if os.path.isfile(chmfile):
140 elif macosxSupport.runningAsOSXApp():
141 # documentation is stored inside the python framework
142 dochome = os.path.join(sys.prefix,
143 'Resources/English.lproj/Documentation/index.html')
144 dochome = os.path.normpath(dochome)
145 if os.path.isfile(dochome):
146 EditorWindow.help_url = dochome
147 if sys.platform == 'darwin':
148 # Safari requires real file:-URLs
149 EditorWindow.help_url = 'file://' + EditorWindow.help_url
151 EditorWindow.help_url = "http://docs.python.org/%d.%d" % sys.version_info[:2]
152 currentTheme=idleConf.CurrentTheme()
154 root = root or flist.root
158 except AttributeError:
160 self.menubar = Menu(root)
161 self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
163 self.tkinter_vars = flist.vars
164 #self.top.instance_dict makes flist.inversedict available to
165 #configDialog.py so it can access all EditorWindow instances
166 self.top.instance_dict = flist.inversedict
168 self.tkinter_vars = {} # keys: Tkinter event names
169 # values: Tkinter variable instances
170 self.top.instance_dict = {}
171 self.recent_files_path = os.path.join(idleConf.GetUserCfgDir(),
173 self.text_frame = text_frame = Frame(top)
174 self.vbar = vbar = Scrollbar(text_frame, name='vbar')
175 self.width = idleConf.GetOption('main','EditorWindow','width')
181 'height': idleConf.GetOption('main', 'EditorWindow', 'height')}
183 # Starting with tk 8.5 we have to set the new tabstyle option
184 # to 'wordprocessor' to achieve the same display of tabs as in
186 text_options['tabstyle'] = 'wordprocessor'
187 self.text = text = MultiCallCreator(Text)(text_frame, **text_options)
188 self.top.focused_widget = self.text
191 self.apply_bindings()
193 self.top.protocol("WM_DELETE_WINDOW", self.close)
194 self.top.bind("<<close-window>>", self.close_event)
195 if macosxSupport.runningAsOSXApp():
196 # Command-W on editorwindows doesn't work without this.
197 text.bind('<<close-window>>', self.close_event)
198 # Some OS X systems have only one mouse button,
199 # so use control-click for pulldown menus there.
200 # (Note, AquaTk defines <2> as the right button if
201 # present and the Tk Text widget already binds <2>.)
202 text.bind("<Control-Button-1>",self.right_menu_event)
204 # Elsewhere, use right-click for pulldown menus.
205 text.bind("<3>",self.right_menu_event)
206 text.bind("<<cut>>", self.cut)
207 text.bind("<<copy>>", self.copy)
208 text.bind("<<paste>>", self.paste)
209 text.bind("<<center-insert>>", self.center_insert_event)
210 text.bind("<<help>>", self.help_dialog)
211 text.bind("<<python-docs>>", self.python_docs)
212 text.bind("<<about-idle>>", self.about_dialog)
213 text.bind("<<open-config-dialog>>", self.config_dialog)
214 text.bind("<<open-module>>", self.open_module)
215 text.bind("<<do-nothing>>", lambda event: "break")
216 text.bind("<<select-all>>", self.select_all)
217 text.bind("<<remove-selection>>", self.remove_selection)
218 text.bind("<<find>>", self.find_event)
219 text.bind("<<find-again>>", self.find_again_event)
220 text.bind("<<find-in-files>>", self.find_in_files_event)
221 text.bind("<<find-selection>>", self.find_selection_event)
222 text.bind("<<replace>>", self.replace_event)
223 text.bind("<<goto-line>>", self.goto_line_event)
224 text.bind("<<smart-backspace>>",self.smart_backspace_event)
225 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
226 text.bind("<<smart-indent>>",self.smart_indent_event)
227 text.bind("<<indent-region>>",self.indent_region_event)
228 text.bind("<<dedent-region>>",self.dedent_region_event)
229 text.bind("<<comment-region>>",self.comment_region_event)
230 text.bind("<<uncomment-region>>",self.uncomment_region_event)
231 text.bind("<<tabify-region>>",self.tabify_region_event)
232 text.bind("<<untabify-region>>",self.untabify_region_event)
233 text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
234 text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
235 text.bind("<Left>", self.move_at_edge_if_selection(0))
236 text.bind("<Right>", self.move_at_edge_if_selection(1))
237 text.bind("<<del-word-left>>", self.del_word_left)
238 text.bind("<<del-word-right>>", self.del_word_right)
239 text.bind("<<beginning-of-line>>", self.home_callback)
242 flist.inversedict[self] = key
244 flist.dict[key] = self
245 text.bind("<<open-new-window>>", self.new_callback)
246 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
247 text.bind("<<open-class-browser>>", self.open_class_browser)
248 text.bind("<<open-path-browser>>", self.open_path_browser)
250 self.set_status_bar()
251 vbar['command'] = text.yview
252 vbar.pack(side=RIGHT, fill=Y)
253 text['yscrollcommand'] = vbar.set
254 fontWeight = 'normal'
255 if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'):
257 text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
258 idleConf.GetOption('main', 'EditorWindow', 'font-size'),
260 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
261 text.pack(side=TOP, fill=BOTH, expand=1)
264 # usetabs true -> literal tab characters are used by indent and
265 # dedent cmds, possibly mixed with spaces if
266 # indentwidth is not a multiple of tabwidth,
267 # which will cause Tabnanny to nag!
268 # false -> tab characters are converted to spaces by indent
269 # and dedent cmds, and ditto TAB keystrokes
270 # Although use-spaces=0 can be configured manually in config-main.def,
271 # configuration of tabs v. spaces is not supported in the configuration
272 # dialog. IDLE promotes the preferred Python indentation: use spaces!
273 usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool')
274 self.usetabs = not usespaces
276 # tabwidth is the display width of a literal tab character.
277 # CAUTION: telling Tk to use anything other than its default
278 # tab setting causes it to use an entirely different tabbing algorithm,
279 # treating tab stops as fixed distances from the left margin.
280 # Nobody expects this, so for now tabwidth should never be changed.
281 self.tabwidth = 8 # must remain 8 until Tk is fixed.
283 # indentwidth is the number of screen characters per indent level.
284 # The recommended Python indentation is four spaces.
285 self.indentwidth = self.tabwidth
286 self.set_notabs_indentwidth()
288 # If context_use_ps1 is true, parsing searches back for a ps1 line;
289 # else searches for a popular (if, def, ...) Python stmt.
290 self.context_use_ps1 = False
292 # When searching backwards for a reliable place to begin parsing,
293 # first start num_context_lines[0] lines back, then
294 # num_context_lines[1] lines back if that didn't work, and so on.
295 # The last value should be huge (larger than the # of lines in a
297 # Making the initial values larger slows things down more often.
298 self.num_context_lines = 50, 500, 5000000
300 self.per = per = self.Percolator(text)
302 self.undo = undo = self.UndoDelegator()
303 per.insertfilter(undo)
304 text.undo_block_start = undo.undo_block_start
305 text.undo_block_stop = undo.undo_block_stop
306 undo.set_saved_change_hook(self.saved_change_hook)
308 # IOBinding implements file I/O and printing functionality
309 self.io = io = self.IOBinding(self)
310 io.set_filename_change_hook(self.filename_change_hook)
312 # Create the recent files submenu
313 self.recent_files_menu = Menu(self.menubar)
314 self.menudict['file'].insert_cascade(3, label='Recent Files',
316 menu=self.recent_files_menu)
317 self.update_recent_files_list()
319 self.color = None # initialized below in self.ResetColorizer
321 if os.path.exists(filename) and not os.path.isdir(filename):
322 io.loadfile(filename)
324 io.set_filename(filename)
325 self.ResetColorizer()
326 self.saved_change_hook()
328 self.set_indentation_params(self.ispythonsource(filename))
330 self.load_extensions()
332 menu = self.menudict.get('windows')
334 end = menu.index("end")
341 WindowList.register_callback(self.postwindowsmenu)
343 # Some abstractions so IDLE extensions are cross-IDE
344 self.askyesno = tkMessageBox.askyesno
345 self.askinteger = tkSimpleDialog.askinteger
346 self.showerror = tkMessageBox.showerror
348 def _filename_to_unicode(self, filename):
349 """convert filename to unicode in order to display it in Tk"""
350 if isinstance(filename, unicode) or not filename:
354 return filename.decode(self.filesystemencoding)
355 except UnicodeDecodeError:
358 return filename.decode(self.encoding)
359 except UnicodeDecodeError:
360 # byte-to-byte conversion
361 return filename.decode('iso8859-1')
363 def new_callback(self, event):
364 dirname, basename = self.io.defaultfilename()
365 self.flist.new(dirname)
368 def home_callback(self, event):
369 if (event.state & 4) != 0 and event.keysym == "Home":
370 # state&4==Control. If <Control-Home>, use the Tk binding.
372 if self.text.index("iomark") and \
373 self.text.compare("iomark", "<=", "insert lineend") and \
374 self.text.compare("insert linestart", "<=", "iomark"):
375 # In Shell on input line, go to just after prompt
376 insertpt = int(self.text.index("iomark").split(".")[1])
378 line = self.text.get("insert linestart", "insert lineend")
379 for insertpt in xrange(len(line)):
380 if line[insertpt] not in (' ','\t'):
384 lineat = int(self.text.index("insert").split('.')[1])
385 if insertpt == lineat:
387 dest = "insert linestart+"+str(insertpt)+"c"
388 if (event.state&1) == 0:
389 # shift was not pressed
390 self.text.tag_remove("sel", "1.0", "end")
392 if not self.text.index("sel.first"):
393 self.text.mark_set("my_anchor", "insert") # there was no previous selection
395 if self.text.compare(self.text.index("sel.first"), "<", self.text.index("insert")):
396 self.text.mark_set("my_anchor", "sel.first") # extend back
398 self.text.mark_set("my_anchor", "sel.last") # extend forward
399 first = self.text.index(dest)
400 last = self.text.index("my_anchor")
401 if self.text.compare(first,">",last):
402 first,last = last,first
403 self.text.tag_remove("sel", "1.0", "end")
404 self.text.tag_add("sel", first, last)
405 self.text.mark_set("insert", dest)
406 self.text.see("insert")
409 def set_status_bar(self):
410 self.status_bar = self.MultiStatusBar(self.top)
411 if macosxSupport.runningAsOSXApp():
412 # Insert some padding to avoid obscuring some of the statusbar
413 # by the resize widget.
414 self.status_bar.set_label('_padding1', ' ', side=RIGHT)
415 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
416 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
417 self.status_bar.pack(side=BOTTOM, fill=X)
418 self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
419 self.text.event_add("<<set-line-and-column>>",
420 "<KeyRelease>", "<ButtonRelease>")
421 self.text.after_idle(self.set_line_and_column)
423 def set_line_and_column(self, event=None):
424 line, column = self.text.index(INSERT).split('.')
425 self.status_bar.set_label('column', 'Col: %s' % column)
426 self.status_bar.set_label('line', 'Ln: %s' % line)
431 ("format", "F_ormat"),
433 ("options", "_Options"),
434 ("windows", "_Windows"),
438 if macosxSupport.runningAsOSXApp():
440 menu_specs[-2] = ("windows", "_Window")
443 def createmenubar(self):
445 self.menudict = menudict = {}
446 for name, label in self.menu_specs:
447 underline, label = prepstr(label)
448 menudict[name] = menu = Menu(mbar, name=name)
449 mbar.add_cascade(label=label, menu=menu, underline=underline)
451 if macosxSupport.isCarbonAquaTk(self.root):
452 # Insert the application menu
453 menudict['application'] = menu = Menu(mbar, name='apple')
454 mbar.add_cascade(label='IDLE', menu=menu)
457 self.base_helpmenu_length = self.menudict['help'].index(END)
458 self.reset_help_menu_entries()
460 def postwindowsmenu(self):
461 # Only called when Windows menu exists
462 menu = self.menudict['windows']
463 end = menu.index("end")
466 if end > self.wmenu_end:
467 menu.delete(self.wmenu_end+1, end)
468 WindowList.add_windows_to_menu(menu)
472 def right_menu_event(self, event):
473 self.text.tag_remove("sel", "1.0", "end")
474 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
479 iswin = sys.platform[:3] == 'win'
481 self.text.config(cursor="arrow")
482 rmenu.tk_popup(event.x_root, event.y_root)
484 self.text.config(cursor="ibeam")
487 # ("Label", "<<virtual-event>>"), ...
488 ("Close", "<<close-window>>"), # Example
491 def make_rmenu(self):
492 rmenu = Menu(self.text, tearoff=0)
493 for label, eventname in self.rmenu_specs:
494 def command(text=self.text, eventname=eventname):
495 text.event_generate(eventname)
496 rmenu.add_command(label=label, command=command)
499 def about_dialog(self, event=None):
500 aboutDialog.AboutDialog(self.top,'About IDLE')
502 def config_dialog(self, event=None):
503 configDialog.ConfigDialog(self.top,'Settings')
505 def help_dialog(self, event=None):
510 helpDialog.display(parent, near=self.top)
512 def python_docs(self, event=None):
513 if sys.platform[:3] == 'win':
515 os.startfile(self.help_url)
516 except WindowsError as why:
517 tkMessageBox.showerror(title='Document Start Failure',
518 message=str(why), parent=self.text)
520 webbrowser.open(self.help_url)
524 self.text.event_generate("<<Cut>>")
527 def copy(self,event):
528 if not self.text.tag_ranges("sel"):
529 # There is no selection, so do nothing and maybe interrupt.
531 self.text.event_generate("<<Copy>>")
534 def paste(self,event):
535 self.text.event_generate("<<Paste>>")
536 self.text.see("insert")
539 def select_all(self, event=None):
540 self.text.tag_add("sel", "1.0", "end-1c")
541 self.text.mark_set("insert", "1.0")
542 self.text.see("insert")
545 def remove_selection(self, event=None):
546 self.text.tag_remove("sel", "1.0", "end")
547 self.text.see("insert")
549 def move_at_edge_if_selection(self, edge_index):
550 """Cursor move begins at start or end of selection
552 When a left/right cursor key is pressed create and return to Tkinter a
553 function which causes a cursor move from the associated edge of the
557 self_text_index = self.text.index
558 self_text_mark_set = self.text.mark_set
559 edges_table = ("sel.first+1c", "sel.last-1c")
560 def move_at_edge(event):
561 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
563 self_text_index("sel.first")
564 self_text_mark_set("insert", edges_table[edge_index])
569 def del_word_left(self, event):
570 self.text.event_generate('<Meta-Delete>')
573 def del_word_right(self, event):
574 self.text.event_generate('<Meta-d>')
577 def find_event(self, event):
578 SearchDialog.find(self.text)
581 def find_again_event(self, event):
582 SearchDialog.find_again(self.text)
585 def find_selection_event(self, event):
586 SearchDialog.find_selection(self.text)
589 def find_in_files_event(self, event):
590 GrepDialog.grep(self.text, self.io, self.flist)
593 def replace_event(self, event):
594 ReplaceDialog.replace(self.text)
597 def goto_line_event(self, event):
599 lineno = tkSimpleDialog.askinteger("Goto",
600 "Go to line number:",parent=text)
606 text.mark_set("insert", "%d.0" % lineno)
609 def open_module(self, event=None):
610 # XXX Shouldn't this be in IOBinding or in FileList?
612 name = self.text.get("sel.first", "sel.last")
617 name = tkSimpleDialog.askstring("Module",
618 "Enter the name of a Python module\n"
619 "to search on sys.path and open:",
620 parent=self.text, initialvalue=name)
625 # XXX Ought to insert current file's directory in front of path
627 (f, file, (suffix, mode, type)) = _find_module(name)
628 except (NameError, ImportError), msg:
629 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
631 if type != imp.PY_SOURCE:
632 tkMessageBox.showerror("Unsupported type",
633 "%s is not a source module" % name, parent=self.text)
638 self.flist.open(file)
640 self.io.loadfile(file)
642 def open_class_browser(self, event=None):
643 filename = self.io.filename
645 tkMessageBox.showerror(
647 "This buffer has no associated filename",
649 self.text.focus_set()
651 head, tail = os.path.split(filename)
652 base, ext = os.path.splitext(tail)
653 from idlelib import ClassBrowser
654 ClassBrowser.ClassBrowser(self.flist, base, [head])
656 def open_path_browser(self, event=None):
657 from idlelib import PathBrowser
658 PathBrowser.PathBrowser(self.flist)
660 def gotoline(self, lineno):
661 if lineno is not None and lineno > 0:
662 self.text.mark_set("insert", "%d.0" % lineno)
663 self.text.tag_remove("sel", "1.0", "end")
664 self.text.tag_add("sel", "insert", "insert +1l")
667 def ispythonsource(self, filename):
668 if not filename or os.path.isdir(filename):
670 base, ext = os.path.splitext(os.path.basename(filename))
671 if os.path.normcase(ext) in (".py", ".pyw"):
679 return line.startswith('#!') and line.find('python') >= 0
681 def close_hook(self):
683 self.flist.unregister_maybe_terminate(self)
686 def set_close_hook(self, close_hook):
687 self.close_hook = close_hook
689 def filename_change_hook(self):
691 self.flist.filename_changed_edit(self)
692 self.saved_change_hook()
693 self.top.update_windowlist_registry(self)
694 self.ResetColorizer()
696 def _addcolorizer(self):
699 if self.ispythonsource(self.io.filename):
700 self.color = self.ColorDelegator()
701 # can add more colorizers here...
703 self.per.removefilter(self.undo)
704 self.per.insertfilter(self.color)
705 self.per.insertfilter(self.undo)
707 def _rmcolorizer(self):
710 self.color.removecolors()
711 self.per.removefilter(self.color)
714 def ResetColorizer(self):
715 "Update the colour theme"
716 # Called from self.filename_change_hook and from configDialog.py
719 theme = idleConf.GetOption('main','Theme','name')
720 normal_colors = idleConf.GetHighlight(theme, 'normal')
721 cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg')
722 select_colors = idleConf.GetHighlight(theme, 'hilite')
724 foreground=normal_colors['foreground'],
725 background=normal_colors['background'],
726 insertbackground=cursor_color,
727 selectforeground=select_colors['foreground'],
728 selectbackground=select_colors['background'],
732 "Update the text widgets' font if it is changed"
733 # Called from configDialog.py
735 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
737 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
738 idleConf.GetOption('main','EditorWindow','font-size'),
741 def RemoveKeybindings(self):
742 "Remove the keybindings before they are changed."
743 # Called from configDialog.py
744 self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
745 for event, keylist in keydefs.items():
746 self.text.event_delete(event, *keylist)
747 for extensionName in self.get_standard_extension_names():
748 xkeydefs = idleConf.GetExtensionBindings(extensionName)
750 for event, keylist in xkeydefs.items():
751 self.text.event_delete(event, *keylist)
753 def ApplyKeybindings(self):
754 "Update the keybindings after they are changed"
755 # Called from configDialog.py
756 self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
757 self.apply_bindings()
758 for extensionName in self.get_standard_extension_names():
759 xkeydefs = idleConf.GetExtensionBindings(extensionName)
761 self.apply_bindings(xkeydefs)
762 #update menu accelerators
764 for menu in self.Bindings.menudefs:
765 menuEventDict[menu[0]] = {}
768 menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
769 for menubarItem in self.menudict.keys():
770 menu = self.menudict[menubarItem]
771 end = menu.index(END) + 1
772 for index in range(0, end):
773 if menu.type(index) == 'command':
774 accel = menu.entrycget(index, 'accelerator')
776 itemName = menu.entrycget(index, 'label')
778 if menubarItem in menuEventDict:
779 if itemName in menuEventDict[menubarItem]:
780 event = menuEventDict[menubarItem][itemName]
782 accel = get_accelerator(keydefs, event)
783 menu.entryconfig(index, accelerator=accel)
785 def set_notabs_indentwidth(self):
786 "Update the indentwidth if changed and not using tabs in this window"
787 # Called from configDialog.py
789 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
792 def reset_help_menu_entries(self):
793 "Update the additional help entries on the Help menu"
794 help_list = idleConf.GetAllExtraHelpSourcesList()
795 helpmenu = self.menudict['help']
796 # first delete the extra help entries, if any
797 helpmenu_length = helpmenu.index(END)
798 if helpmenu_length > self.base_helpmenu_length:
799 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
802 helpmenu.add_separator()
803 for entry in help_list:
804 cmd = self.__extra_help_callback(entry[1])
805 helpmenu.add_command(label=entry[0], command=cmd)
806 # and update the menu dictionary
807 self.menudict['help'] = helpmenu
809 def __extra_help_callback(self, helpfile):
810 "Create a callback with the helpfile value frozen at definition time"
811 def display_extra_help(helpfile=helpfile):
812 if not helpfile.startswith(('www', 'http')):
813 helpfile = os.path.normpath(helpfile)
814 if sys.platform[:3] == 'win':
816 os.startfile(helpfile)
817 except WindowsError as why:
818 tkMessageBox.showerror(title='Document Start Failure',
819 message=str(why), parent=self.text)
821 webbrowser.open(helpfile)
822 return display_extra_help
824 def update_recent_files_list(self, new_file=None):
825 "Load and update the recent files list and menus"
827 if os.path.exists(self.recent_files_path):
828 rf_list_file = open(self.recent_files_path,'r')
830 rf_list = rf_list_file.readlines()
834 new_file = os.path.abspath(new_file) + '\n'
835 if new_file in rf_list:
836 rf_list.remove(new_file) # move to top
837 rf_list.insert(0, new_file)
838 # clean and save the recent files list
841 if '\0' in path or not os.path.exists(path[0:-1]):
842 bad_paths.append(path)
843 rf_list = [path for path in rf_list if path not in bad_paths]
844 ulchars = "1234567890ABCDEFGHIJK"
845 rf_list = rf_list[0:len(ulchars)]
847 with open(self.recent_files_path, 'w') as rf_file:
848 rf_file.writelines(rf_list)
849 except IOError as err:
850 if not getattr(self.root, "recentfilelist_error_displayed", False):
851 self.root.recentfilelist_error_displayed = True
852 tkMessageBox.showerror(title='IDLE Error',
853 message='Unable to update Recent Files list:\n%s'
856 # for each edit window instance, construct the recent files menu
857 for instance in self.top.instance_dict.keys():
858 menu = instance.recent_files_menu
859 menu.delete(1, END) # clear, and rebuild:
860 for i, file_name in enumerate(rf_list):
861 file_name = file_name.rstrip() # zap \n
862 # make unicode string to display non-ASCII chars correctly
863 ufile_name = self._filename_to_unicode(file_name)
864 callback = instance.__recent_file_callback(file_name)
865 menu.add_command(label=ulchars[i] + " " + ufile_name,
869 def __recent_file_callback(self, file_name):
870 def open_recent_file(fn_closure=file_name):
871 self.io.open(editFile=fn_closure)
872 return open_recent_file
874 def saved_change_hook(self):
875 short = self.short_title()
876 long = self.long_title()
878 title = short + " - " + long
885 icon = short or long or title
886 if not self.get_saved():
887 title = "*%s*" % title
889 self.top.wm_title(title)
890 self.top.wm_iconname(icon)
893 return self.undo.get_saved()
895 def set_saved(self, flag):
896 self.undo.set_saved(flag)
898 def reset_undo(self):
899 self.undo.reset_undo()
901 def short_title(self):
902 filename = self.io.filename
904 filename = os.path.basename(filename)
905 # return unicode string to display non-ASCII chars correctly
906 return self._filename_to_unicode(filename)
908 def long_title(self):
909 # return unicode string to display non-ASCII chars correctly
910 return self._filename_to_unicode(self.io.filename or "")
912 def center_insert_event(self, event):
915 def center(self, mark="insert"):
917 top, bot = self.getwindowlines()
918 lineno = self.getlineno(mark)
920 newtop = max(1, lineno - height//2)
921 text.yview(float(newtop))
923 def getwindowlines(self):
925 top = self.getlineno("@0,0")
926 bot = self.getlineno("@0,65535")
927 if top == bot and text.winfo_height() == 1:
928 # Geometry manager hasn't run yet
929 height = int(text['height'])
930 bot = top + height - 1
933 def getlineno(self, mark="insert"):
935 return int(float(text.index(mark)))
937 def get_geometry(self):
938 "Return (width, height, x, y)"
939 geom = self.top.wm_geometry()
940 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
941 tuple = (map(int, m.groups()))
944 def close_event(self, event):
949 if not self.get_saved():
950 if self.top.state()!='normal':
954 return self.io.maybesave()
957 reply = self.maybesave()
958 if str(reply) != "cancel":
964 self.update_recent_files_list(new_file=self.io.filename)
965 WindowList.unregister_callback(self.postwindowsmenu)
966 self.unload_extensions()
971 self.color.close(False)
974 self.tkinter_vars = None
979 # unless override: unregister from flist, terminate if last window
982 def load_extensions(self):
984 self.load_standard_extensions()
986 def unload_extensions(self):
987 for ins in self.extensions.values():
988 if hasattr(ins, "close"):
992 def load_standard_extensions(self):
993 for name in self.get_standard_extension_names():
995 self.load_extension(name)
997 print "Failed to load extension", repr(name)
999 traceback.print_exc()
1001 def get_standard_extension_names(self):
1002 return idleConf.GetExtensions(editor_only=True)
1004 def load_extension(self, name):
1006 mod = __import__(name, globals(), locals(), [])
1008 print "\nFailed to import extension: ", name
1010 cls = getattr(mod, name)
1011 keydefs = idleConf.GetExtensionBindings(name)
1012 if hasattr(cls, "menudefs"):
1013 self.fill_menus(cls.menudefs, keydefs)
1015 self.extensions[name] = ins
1017 self.apply_bindings(keydefs)
1018 for vevent in keydefs.keys():
1019 methodname = vevent.replace("-", "_")
1020 while methodname[:1] == '<':
1021 methodname = methodname[1:]
1022 while methodname[-1:] == '>':
1023 methodname = methodname[:-1]
1024 methodname = methodname + "_event"
1025 if hasattr(ins, methodname):
1026 self.text.bind(vevent, getattr(ins, methodname))
1028 def apply_bindings(self, keydefs=None):
1030 keydefs = self.Bindings.default_keydefs
1032 text.keydefs = keydefs
1033 for event, keylist in keydefs.items():
1035 text.event_add(event, *keylist)
1037 def fill_menus(self, menudefs=None, keydefs=None):
1038 """Add appropriate entries to the menus and submenus
1040 Menus that are absent or None in self.menudict are ignored.
1042 if menudefs is None:
1043 menudefs = self.Bindings.menudefs
1045 keydefs = self.Bindings.default_keydefs
1046 menudict = self.menudict
1048 for mname, entrylist in menudefs:
1049 menu = menudict.get(mname)
1052 for entry in entrylist:
1054 menu.add_separator()
1056 label, eventname = entry
1057 checkbutton = (label[:1] == '!')
1060 underline, label = prepstr(label)
1061 accelerator = get_accelerator(keydefs, eventname)
1062 def command(text=text, eventname=eventname):
1063 text.event_generate(eventname)
1065 var = self.get_var_obj(eventname, BooleanVar)
1066 menu.add_checkbutton(label=label, underline=underline,
1067 command=command, accelerator=accelerator,
1070 menu.add_command(label=label, underline=underline,
1072 accelerator=accelerator)
1074 def getvar(self, name):
1075 var = self.get_var_obj(name)
1080 raise NameError, name
1082 def setvar(self, name, value, vartype=None):
1083 var = self.get_var_obj(name, vartype)
1087 raise NameError, name
1089 def get_var_obj(self, name, vartype=None):
1090 var = self.tkinter_vars.get(name)
1091 if not var and vartype:
1092 # create a Tkinter variable object with self.text as master:
1093 self.tkinter_vars[name] = var = vartype(self.text)
1096 # Tk implementations of "virtual text methods" -- each platform
1097 # reusing IDLE's support code needs to define these for its GUI's
1100 # Is character at text_index in a Python string? Return 0 for
1101 # "guaranteed no", true for anything else. This info is expensive
1102 # to compute ab initio, but is probably already known by the
1103 # platform's colorizer.
1105 def is_char_in_string(self, text_index):
1107 # Return true iff colorizer hasn't (re)gotten this far
1108 # yet, or the character is tagged as being in a string
1109 return self.text.tag_prevrange("TODO", text_index) or \
1110 "STRING" in self.text.tag_names(text_index)
1112 # The colorizer is missing: assume the worst
1115 # If a selection is defined in the text widget, return (start,
1116 # end) as Tkinter text indices, otherwise return (None, None)
1117 def get_selection_indices(self):
1119 first = self.text.index("sel.first")
1120 last = self.text.index("sel.last")
1125 # Return the text widget's current view of what a tab stop means
1126 # (equivalent width in spaces).
1128 def get_tabwidth(self):
1129 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
1132 # Set the text widget's current view of what a tab stop means.
1134 def set_tabwidth(self, newtabwidth):
1136 if self.get_tabwidth() != newtabwidth:
1137 pixels = text.tk.call("font", "measure", text["font"],
1138 "-displayof", text.master,
1140 text.configure(tabs=pixels)
1142 # If ispythonsource and guess are true, guess a good value for
1143 # indentwidth based on file content (if possible), and if
1144 # indentwidth != tabwidth set usetabs false.
1145 # In any case, adjust the Text widget's view of what a tab
1148 def set_indentation_params(self, ispythonsource, guess=True):
1149 if guess and ispythonsource:
1150 i = self.guess_indent()
1152 self.indentwidth = i
1153 if self.indentwidth != self.tabwidth:
1154 self.usetabs = False
1155 self.set_tabwidth(self.tabwidth)
1157 def smart_backspace_event(self, event):
1159 first, last = self.get_selection_indices()
1161 text.delete(first, last)
1162 text.mark_set("insert", first)
1164 # Delete whitespace left, until hitting a real char or closest
1165 # preceding virtual tab stop.
1166 chars = text.get("insert linestart", "insert")
1168 if text.compare("insert", ">", "1.0"):
1169 # easy: delete preceding newline
1170 text.delete("insert-1c")
1172 text.bell() # at start of buffer
1174 if chars[-1] not in " \t":
1175 # easy: delete preceding real char
1176 text.delete("insert-1c")
1178 # Ick. It may require *inserting* spaces if we back up over a
1179 # tab character! This is written to be clear, not fast.
1180 tabwidth = self.tabwidth
1181 have = len(chars.expandtabs(tabwidth))
1183 want = ((have - 1) // self.indentwidth) * self.indentwidth
1184 # Debug prompt is multilined....
1185 if self.context_use_ps1:
1186 last_line_of_prompt = sys.ps1.split('\n')[-1]
1188 last_line_of_prompt = ''
1191 if chars == last_line_of_prompt:
1194 ncharsdeleted = ncharsdeleted + 1
1195 have = len(chars.expandtabs(tabwidth))
1196 if have <= want or chars[-1] not in " \t":
1198 text.undo_block_start()
1199 text.delete("insert-%dc" % ncharsdeleted, "insert")
1201 text.insert("insert", ' ' * (want - have))
1202 text.undo_block_stop()
1205 def smart_indent_event(self, event):
1206 # if intraline selection:
1208 # elif multiline selection:
1213 first, last = self.get_selection_indices()
1214 text.undo_block_start()
1217 if index2line(first) != index2line(last):
1218 return self.indent_region_event(event)
1219 text.delete(first, last)
1220 text.mark_set("insert", first)
1221 prefix = text.get("insert linestart", "insert")
1222 raw, effective = classifyws(prefix, self.tabwidth)
1223 if raw == len(prefix):
1224 # only whitespace to the left
1225 self.reindent_to(effective + self.indentwidth)
1227 # tab to the next 'stop' within or to right of line's text:
1231 effective = len(prefix.expandtabs(self.tabwidth))
1232 n = self.indentwidth
1233 pad = ' ' * (n - effective % n)
1234 text.insert("insert", pad)
1238 text.undo_block_stop()
1240 def newline_and_indent_event(self, event):
1242 first, last = self.get_selection_indices()
1243 text.undo_block_start()
1246 text.delete(first, last)
1247 text.mark_set("insert", first)
1248 line = text.get("insert linestart", "insert")
1250 while i < n and line[i] in " \t":
1253 # the cursor is in or at leading indentation in a continuation
1254 # line; just inject an empty line at the start
1255 text.insert("insert linestart", '\n')
1258 # strip whitespace before insert point unless it's in the prompt
1260 last_line_of_prompt = sys.ps1.split('\n')[-1]
1261 while line and line[-1] in " \t" and line != last_line_of_prompt:
1265 text.delete("insert - %d chars" % i, "insert")
1266 # strip whitespace after insert point
1267 while text.get("insert") in " \t":
1268 text.delete("insert")
1270 text.insert("insert", '\n')
1272 # adjust indentation for continuations and block
1273 # open/close first need to find the last stmt
1274 lno = index2line(text.index('insert'))
1275 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1276 if not self.context_use_ps1:
1277 for context in self.num_context_lines:
1278 startat = max(lno - context, 1)
1279 startatindex = repr(startat) + ".0"
1280 rawtext = text.get(startatindex, "insert")
1282 bod = y.find_good_parse_start(
1283 self.context_use_ps1,
1284 self._build_char_in_string_func(startatindex))
1285 if bod is not None or startat == 1:
1289 r = text.tag_prevrange("console", "insert")
1293 startatindex = "1.0"
1294 rawtext = text.get(startatindex, "insert")
1298 c = y.get_continuation_type()
1299 if c != PyParse.C_NONE:
1300 # The current stmt hasn't ended yet.
1301 if c == PyParse.C_STRING_FIRST_LINE:
1302 # after the first line of a string; do not indent at all
1304 elif c == PyParse.C_STRING_NEXT_LINES:
1305 # inside a string which started before this line;
1306 # just mimic the current indent
1307 text.insert("insert", indent)
1308 elif c == PyParse.C_BRACKET:
1309 # line up with the first (if any) element of the
1310 # last open bracket structure; else indent one
1311 # level beyond the indent of the line with the
1313 self.reindent_to(y.compute_bracket_indent())
1314 elif c == PyParse.C_BACKSLASH:
1315 # if more than one line in this stmt already, just
1316 # mimic the current indent; else if initial line
1317 # has a start on an assignment stmt, indent to
1318 # beyond leftmost =; else to beyond first chunk of
1319 # non-whitespace on initial line
1320 if y.get_num_lines_in_stmt() > 1:
1321 text.insert("insert", indent)
1323 self.reindent_to(y.compute_backslash_indent())
1325 assert 0, "bogus continuation type %r" % (c,)
1328 # This line starts a brand new stmt; indent relative to
1329 # indentation of initial line of closest preceding
1331 indent = y.get_base_indent_string()
1332 text.insert("insert", indent)
1333 if y.is_block_opener():
1334 self.smart_indent_event(event)
1335 elif indent and y.is_block_closer():
1336 self.smart_backspace_event(event)
1340 text.undo_block_stop()
1342 # Our editwin provides a is_char_in_string function that works
1343 # with a Tk text index, but PyParse only knows about offsets into
1344 # a string. This builds a function for PyParse that accepts an
1347 def _build_char_in_string_func(self, startindex):
1348 def inner(offset, _startindex=startindex,
1349 _icis=self.is_char_in_string):
1350 return _icis(_startindex + "+%dc" % offset)
1353 def indent_region_event(self, event):
1354 head, tail, chars, lines = self.get_region()
1355 for pos in range(len(lines)):
1358 raw, effective = classifyws(line, self.tabwidth)
1359 effective = effective + self.indentwidth
1360 lines[pos] = self._make_blanks(effective) + line[raw:]
1361 self.set_region(head, tail, chars, lines)
1364 def dedent_region_event(self, event):
1365 head, tail, chars, lines = self.get_region()
1366 for pos in range(len(lines)):
1369 raw, effective = classifyws(line, self.tabwidth)
1370 effective = max(effective - self.indentwidth, 0)
1371 lines[pos] = self._make_blanks(effective) + line[raw:]
1372 self.set_region(head, tail, chars, lines)
1375 def comment_region_event(self, event):
1376 head, tail, chars, lines = self.get_region()
1377 for pos in range(len(lines) - 1):
1379 lines[pos] = '##' + line
1380 self.set_region(head, tail, chars, lines)
1382 def uncomment_region_event(self, event):
1383 head, tail, chars, lines = self.get_region()
1384 for pos in range(len(lines)):
1388 if line[:2] == '##':
1390 elif line[:1] == '#':
1393 self.set_region(head, tail, chars, lines)
1395 def tabify_region_event(self, event):
1396 head, tail, chars, lines = self.get_region()
1397 tabwidth = self._asktabwidth()
1398 for pos in range(len(lines)):
1401 raw, effective = classifyws(line, tabwidth)
1402 ntabs, nspaces = divmod(effective, tabwidth)
1403 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1404 self.set_region(head, tail, chars, lines)
1406 def untabify_region_event(self, event):
1407 head, tail, chars, lines = self.get_region()
1408 tabwidth = self._asktabwidth()
1409 for pos in range(len(lines)):
1410 lines[pos] = lines[pos].expandtabs(tabwidth)
1411 self.set_region(head, tail, chars, lines)
1413 def toggle_tabs_event(self, event):
1416 "Turn tabs " + ("on", "off")[self.usetabs] +
1417 "?\nIndent width " +
1418 ("will be", "remains at")[self.usetabs] + " 8." +
1419 "\n Note: a tab is always 8 columns",
1421 self.usetabs = not self.usetabs
1422 # Try to prevent inconsistent indentation.
1423 # User must change indent width manually after using tabs.
1424 self.indentwidth = 8
1427 # XXX this isn't bound to anything -- see tabwidth comments
1428 ## def change_tabwidth_event(self, event):
1429 ## new = self._asktabwidth()
1430 ## if new != self.tabwidth:
1431 ## self.tabwidth = new
1432 ## self.set_indentation_params(0, guess=0)
1435 def change_indentwidth_event(self, event):
1436 new = self.askinteger(
1438 "New indent width (2-16)\n(Always use 8 when using tabs)",
1440 initialvalue=self.indentwidth,
1443 if new and new != self.indentwidth and not self.usetabs:
1444 self.indentwidth = new
1447 def get_region(self):
1449 first, last = self.get_selection_indices()
1451 head = text.index(first + " linestart")
1452 tail = text.index(last + "-1c lineend +1c")
1454 head = text.index("insert linestart")
1455 tail = text.index("insert lineend +1c")
1456 chars = text.get(head, tail)
1457 lines = chars.split("\n")
1458 return head, tail, chars, lines
1460 def set_region(self, head, tail, chars, lines):
1462 newchars = "\n".join(lines)
1463 if newchars == chars:
1466 text.tag_remove("sel", "1.0", "end")
1467 text.mark_set("insert", head)
1468 text.undo_block_start()
1469 text.delete(head, tail)
1470 text.insert(head, newchars)
1471 text.undo_block_stop()
1472 text.tag_add("sel", head, "insert")
1474 # Make string that displays as n leading blanks.
1476 def _make_blanks(self, n):
1478 ntabs, nspaces = divmod(n, self.tabwidth)
1479 return '\t' * ntabs + ' ' * nspaces
1483 # Delete from beginning of line to insert point, then reinsert
1484 # column logical (meaning use tabs if appropriate) spaces.
1486 def reindent_to(self, column):
1488 text.undo_block_start()
1489 if text.compare("insert linestart", "!=", "insert"):
1490 text.delete("insert linestart", "insert")
1492 text.insert("insert", self._make_blanks(column))
1493 text.undo_block_stop()
1495 def _asktabwidth(self):
1496 return self.askinteger(
1498 "Columns per tab? (2-16)",
1500 initialvalue=self.indentwidth,
1502 maxvalue=16) or self.tabwidth
1504 # Guess indentwidth from text content.
1505 # Return guessed indentwidth. This should not be believed unless
1506 # it's in a reasonable range (e.g., it will be 0 if no indented
1507 # blocks are found).
1509 def guess_indent(self):
1510 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1511 if opener and indented:
1512 raw, indentsmall = classifyws(opener, self.tabwidth)
1513 raw, indentlarge = classifyws(indented, self.tabwidth)
1515 indentsmall = indentlarge = 0
1516 return indentlarge - indentsmall
1518 # "line.col" -> line, as an int
1519 def index2line(index):
1520 return int(float(index))
1522 # Look at the leading whitespace in s.
1523 # Return pair (# of leading ws characters,
1524 # effective # of leading blanks after expanding
1525 # tabs to width tabwidth)
1527 def classifyws(s, tabwidth):
1532 effective = effective + 1
1535 effective = (effective // tabwidth + 1) * tabwidth
1538 return raw, effective
1541 _tokenize = tokenize
1544 class IndentSearcher(object):
1546 # .run() chews over the Text widget, looking for a block opener
1547 # and the stmt following it. Returns a pair,
1548 # (line containing block opener, line containing stmt)
1549 # Either or both may be None.
1551 def __init__(self, text, tabwidth):
1553 self.tabwidth = tabwidth
1554 self.i = self.finished = 0
1555 self.blkopenline = self.indentedline = None
1560 i = self.i = self.i + 1
1561 mark = repr(i) + ".0"
1562 if self.text.compare(mark, ">=", "end"):
1564 return self.text.get(mark, mark + " lineend+1c")
1566 def tokeneater(self, type, token, start, end, line,
1567 INDENT=_tokenize.INDENT,
1568 NAME=_tokenize.NAME,
1569 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1572 elif type == NAME and token in OPENERS:
1573 self.blkopenline = line
1574 elif type == INDENT and self.blkopenline:
1575 self.indentedline = line
1579 save_tabsize = _tokenize.tabsize
1580 _tokenize.tabsize = self.tabwidth
1583 _tokenize.tokenize(self.readline, self.tokeneater)
1584 except _tokenize.TokenError:
1585 # since we cut off the tokenizer early, we can trigger
1589 _tokenize.tabsize = save_tabsize
1590 return self.blkopenline, self.indentedline
1592 ### end autoindent code ###
1595 # Helper to extract the underscore from a string, e.g.
1596 # prepstr("Co_py") returns (2, "Copy").
1605 'bracketright': ']',
1609 def get_accelerator(keydefs, eventname):
1610 keylist = keydefs.get(eventname)
1611 # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5
1613 if (not keylist) or (macosxSupport.runningAsOSXApp() and eventname in {
1616 "<<change-indentwidth>>"}):
1619 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
1620 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1621 s = re.sub("Key-", "", s)
1622 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1623 s = re.sub("Control-", "Ctrl-", s)
1624 s = re.sub("-", "+", s)
1625 s = re.sub("><", " ", s)
1626 s = re.sub("<", "", s)
1627 s = re.sub(">", "", s)
1631 def fixwordbreaks(root):
1632 # Make sure that Tk's double-click and next/previous word
1633 # operations use our definition of a word (i.e. an identifier)
1635 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1636 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1637 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1645 filename = sys.argv[1]
1648 edit = EditorWindow(root=root, filename=filename)
1649 edit.set_close_hook(root.quit)
1650 edit.text.bind("<<close-all-windows>>", edit.close_event)
1654 if __name__ == '__main__':