1import sys 2import os 3import re 4import imp 5from Tkinter import * 6import tkSimpleDialog 7import tkMessageBox 8import webbrowser 9 10from idlelib.MultiCall import MultiCallCreator 11from idlelib import idlever 12from idlelib import WindowList 13from idlelib import SearchDialog 14from idlelib import GrepDialog 15from idlelib import ReplaceDialog 16from idlelib import PyParse 17from idlelib.configHandler import idleConf 18from idlelib import aboutDialog, textView, configDialog 19from idlelib import macosxSupport 20 21# The default tab setting for a Text widget, in average-width characters. 22TK_TABWIDTH_DEFAULT = 8 23 24def _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) 28 if micro: 29 release += '%s' % (micro,) 30 if level == 'candidate': 31 release += 'rc%s' % (serial,) 32 elif level != 'final': 33 release += '%s%s' % (level[0], serial) 34 return release 35 36def _find_module(fullname, path=None): 37 """Version of imp.find_module() that handles hierarchical module names""" 38 39 file = None 40 for tgt in fullname.split('.'): 41 if file is not None: 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) 47 try: 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) 55 try: 56 filename = m.__file__ 57 except AttributeError: 58 pass 59 else: 60 file = None 61 base, ext = os.path.splitext(filename) 62 if ext == '.pyc': 63 ext = '.py' 64 filename = base + ext 65 descr = filename, None, imp.PY_SOURCE 66 return file, filename, descr 67 68 69class HelpDialog(object): 70 71 def __init__(self): 72 self.parent = None # parent of help window 73 self.dlg = None # the help window iteself 74 75 def display(self, parent, near=None): 76 """ Display the help dialog. 77 78 parent - parent widget for the help window 79 80 near - a Toplevel widget (e.g. EditorWindow or PyShell) 81 to use as a reference for placing the help window 82 """ 83 if self.dlg is None: 84 self.show_dialog(parent) 85 if near: 86 self.nearwindow(near) 87 88 def show_dialog(self, parent): 89 self.parent = 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, '+') 93 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 98 dlg = self.dlg 99 geom = (near.winfo_rootx() + 10, near.winfo_rooty() + 10) 100 dlg.withdraw() 101 dlg.geometry("=+%d+%d" % geom) 102 dlg.deiconify() 103 dlg.lift() 104 105 def destroy(self, ev=None): 106 self.dlg = None 107 self.parent = None 108 109helpDialog = HelpDialog() # singleton instance 110 111 112class 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 120 121 help_url = None 122 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' 131 else: 132 basepath = '/usr/share/doc/' # standard location 133 dochome = os.path.join(basepath, pyver, 134 'Doc', 'index.html') 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): 139 dochome = 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 150 else: 151 EditorWindow.help_url = "http://docs.python.org/%d.%d" % sys.version_info[:2] 152 currentTheme=idleConf.CurrentTheme() 153 self.flist = flist 154 root = root or flist.root 155 self.root = root 156 try: 157 sys.ps1 158 except AttributeError: 159 sys.ps1 = '>>> ' 160 self.menubar = Menu(root) 161 self.top = top = WindowList.ListedToplevel(root, menu=self.menubar) 162 if flist: 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 167 else: 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(), 172 'recent-files.lst') 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', type='int') 176 text_options = { 177 'name': 'text', 178 'padx': 5, 179 'wrap': 'none', 180 'width': self.width, 181 'height': idleConf.GetOption('main', 'EditorWindow', 'height', type='int')} 182 if TkVersion >= 8.5: 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 185 # older tk versions. 186 text_options['tabstyle'] = 'wordprocessor' 187 self.text = text = MultiCallCreator(Text)(text_frame, **text_options) 188 self.top.focused_widget = self.text 189 190 self.createmenubar() 191 self.apply_bindings() 192 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) 203 else: 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) 240 241 if flist: 242 flist.inversedict[self] = key 243 if 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) 249 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'): 256 fontWeight='bold' 257 text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'), 258 idleConf.GetOption('main', 'EditorWindow', 259 'font-size', type='int'), 260 fontWeight)) 261 text_frame.pack(side=LEFT, fill=BOTH, expand=1) 262 text.pack(side=TOP, fill=BOTH, expand=1) 263 text.focus_set() 264 265 # usetabs true -> literal tab characters are used by indent and 266 # dedent cmds, possibly mixed with spaces if 267 # indentwidth is not a multiple of tabwidth, 268 # which will cause Tabnanny to nag! 269 # false -> tab characters are converted to spaces by indent 270 # and dedent cmds, and ditto TAB keystrokes 271 # Although use-spaces=0 can be configured manually in config-main.def, 272 # configuration of tabs v. spaces is not supported in the configuration 273 # dialog. IDLE promotes the preferred Python indentation: use spaces! 274 usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool') 275 self.usetabs = not usespaces 276 277 # tabwidth is the display width of a literal tab character. 278 # CAUTION: telling Tk to use anything other than its default 279 # tab setting causes it to use an entirely different tabbing algorithm, 280 # treating tab stops as fixed distances from the left margin. 281 # Nobody expects this, so for now tabwidth should never be changed. 282 self.tabwidth = 8 # must remain 8 until Tk is fixed. 283 284 # indentwidth is the number of screen characters per indent level. 285 # The recommended Python indentation is four spaces. 286 self.indentwidth = self.tabwidth 287 self.set_notabs_indentwidth() 288 289 # If context_use_ps1 is true, parsing searches back for a ps1 line; 290 # else searches for a popular (if, def, ...) Python stmt. 291 self.context_use_ps1 = False 292 293 # When searching backwards for a reliable place to begin parsing, 294 # first start num_context_lines[0] lines back, then 295 # num_context_lines[1] lines back if that didn't work, and so on. 296 # The last value should be huge (larger than the # of lines in a 297 # conceivable file). 298 # Making the initial values larger slows things down more often. 299 self.num_context_lines = 50, 500, 5000000 300 301 self.per = per = self.Percolator(text) 302 303 self.undo = undo = self.UndoDelegator() 304 per.insertfilter(undo) 305 text.undo_block_start = undo.undo_block_start 306 text.undo_block_stop = undo.undo_block_stop 307 undo.set_saved_change_hook(self.saved_change_hook) 308 309 # IOBinding implements file I/O and printing functionality 310 self.io = io = self.IOBinding(self) 311 io.set_filename_change_hook(self.filename_change_hook) 312 313 # Create the recent files submenu 314 self.recent_files_menu = Menu(self.menubar) 315 self.menudict['file'].insert_cascade(3, label='Recent Files', 316 underline=0, 317 menu=self.recent_files_menu) 318 self.update_recent_files_list() 319 320 self.color = None # initialized below in self.ResetColorizer 321 if filename: 322 if os.path.exists(filename) and not os.path.isdir(filename): 323 io.loadfile(filename) 324 else: 325 io.set_filename(filename) 326 self.ResetColorizer() 327 self.saved_change_hook() 328 329 self.set_indentation_params(self.ispythonsource(filename)) 330 331 self.load_extensions() 332 333 menu = self.menudict.get('windows') 334 if menu: 335 end = menu.index("end") 336 if end is None: 337 end = -1 338 if end >= 0: 339 menu.add_separator() 340 end = end + 1 341 self.wmenu_end = end 342 WindowList.register_callback(self.postwindowsmenu) 343 344 # Some abstractions so IDLE extensions are cross-IDE 345 self.askyesno = tkMessageBox.askyesno 346 self.askinteger = tkSimpleDialog.askinteger 347 self.showerror = tkMessageBox.showerror 348 349 def _filename_to_unicode(self, filename): 350 """convert filename to unicode in order to display it in Tk""" 351 if isinstance(filename, unicode) or not filename: 352 return filename 353 else: 354 try: 355 return filename.decode(self.filesystemencoding) 356 except UnicodeDecodeError: 357 # XXX 358 try: 359 return filename.decode(self.encoding) 360 except UnicodeDecodeError: 361 # byte-to-byte conversion 362 return filename.decode('iso8859-1') 363 364 def new_callback(self, event): 365 dirname, basename = self.io.defaultfilename() 366 self.flist.new(dirname) 367 return "break" 368 369 def home_callback(self, event): 370 if (event.state & 4) != 0 and event.keysym == "Home": 371 # state&4==Control. If <Control-Home>, use the Tk binding. 372 return 373 if self.text.index("iomark") and \ 374 self.text.compare("iomark", "<=", "insert lineend") and \ 375 self.text.compare("insert linestart", "<=", "iomark"): 376 # In Shell on input line, go to just after prompt 377 insertpt = int(self.text.index("iomark").split(".")[1]) 378 else: 379 line = self.text.get("insert linestart", "insert lineend") 380 for insertpt in xrange(len(line)): 381 if line[insertpt] not in (' ','\t'): 382 break 383 else: 384 insertpt=len(line) 385 lineat = int(self.text.index("insert").split('.')[1]) 386 if insertpt == lineat: 387 insertpt = 0 388 dest = "insert linestart+"+str(insertpt)+"c" 389 if (event.state&1) == 0: 390 # shift was not pressed 391 self.text.tag_remove("sel", "1.0", "end") 392 else: 393 if not self.text.index("sel.first"): 394 self.text.mark_set("my_anchor", "insert") # there was no previous selection 395 else: 396 if self.text.compare(self.text.index("sel.first"), "<", self.text.index("insert")): 397 self.text.mark_set("my_anchor", "sel.first") # extend back 398 else: 399 self.text.mark_set("my_anchor", "sel.last") # extend forward 400 first = self.text.index(dest) 401 last = self.text.index("my_anchor") 402 if self.text.compare(first,">",last): 403 first,last = last,first 404 self.text.tag_remove("sel", "1.0", "end") 405 self.text.tag_add("sel", first, last) 406 self.text.mark_set("insert", dest) 407 self.text.see("insert") 408 return "break" 409 410 def set_status_bar(self): 411 self.status_bar = self.MultiStatusBar(self.top) 412 if macosxSupport.runningAsOSXApp(): 413 # Insert some padding to avoid obscuring some of the statusbar 414 # by the resize widget. 415 self.status_bar.set_label('_padding1', ' ', side=RIGHT) 416 self.status_bar.set_label('column', 'Col: ?', side=RIGHT) 417 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT) 418 self.status_bar.pack(side=BOTTOM, fill=X) 419 self.text.bind("<<set-line-and-column>>", self.set_line_and_column) 420 self.text.event_add("<<set-line-and-column>>", 421 "<KeyRelease>", "<ButtonRelease>") 422 self.text.after_idle(self.set_line_and_column) 423 424 def set_line_and_column(self, event=None): 425 line, column = self.text.index(INSERT).split('.') 426 self.status_bar.set_label('column', 'Col: %s' % column) 427 self.status_bar.set_label('line', 'Ln: %s' % line) 428 429 menu_specs = [ 430 ("file", "_File"), 431 ("edit", "_Edit"), 432 ("format", "F_ormat"), 433 ("run", "_Run"), 434 ("options", "_Options"), 435 ("windows", "_Windows"), 436 ("help", "_Help"), 437 ] 438 439 if macosxSupport.runningAsOSXApp(): 440 del menu_specs[-3] 441 menu_specs[-2] = ("windows", "_Window") 442 443 444 def createmenubar(self): 445 mbar = self.menubar 446 self.menudict = menudict = {} 447 for name, label in self.menu_specs: 448 underline, label = prepstr(label) 449 menudict[name] = menu = Menu(mbar, name=name) 450 mbar.add_cascade(label=label, menu=menu, underline=underline) 451 452 if macosxSupport.isCarbonAquaTk(self.root): 453 # Insert the application menu 454 menudict['application'] = menu = Menu(mbar, name='apple') 455 mbar.add_cascade(label='IDLE', menu=menu) 456 457 self.fill_menus() 458 self.base_helpmenu_length = self.menudict['help'].index(END) 459 self.reset_help_menu_entries() 460 461 def postwindowsmenu(self): 462 # Only called when Windows menu exists 463 menu = self.menudict['windows'] 464 end = menu.index("end") 465 if end is None: 466 end = -1 467 if end > self.wmenu_end: 468 menu.delete(self.wmenu_end+1, end) 469 WindowList.add_windows_to_menu(menu) 470 471 rmenu = None 472 473 def right_menu_event(self, event): 474 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y)) 475 if not self.rmenu: 476 self.make_rmenu() 477 rmenu = self.rmenu 478 self.event = event 479 iswin = sys.platform[:3] == 'win' 480 if iswin: 481 self.text.config(cursor="arrow") 482 483 for item in self.rmenu_specs: 484 try: 485 label, eventname, verify_state = item 486 except ValueError: # see issue1207589 487 continue 488 489 if verify_state is None: 490 continue 491 state = getattr(self, verify_state)() 492 rmenu.entryconfigure(label, state=state) 493 494 rmenu.tk_popup(event.x_root, event.y_root) 495 if iswin: 496 self.text.config(cursor="ibeam") 497 498 rmenu_specs = [ 499 # ("Label", "<<virtual-event>>", "statefuncname"), ... 500 ("Close", "<<close-window>>", None), # Example 501 ] 502 503 def make_rmenu(self): 504 rmenu = Menu(self.text, tearoff=0) 505 for item in self.rmenu_specs: 506 label, eventname = item[0], item[1] 507 if label is not None: 508 def command(text=self.text, eventname=eventname): 509 text.event_generate(eventname) 510 rmenu.add_command(label=label, command=command) 511 else: 512 rmenu.add_separator() 513 self.rmenu = rmenu 514 515 def rmenu_check_cut(self): 516 return self.rmenu_check_copy() 517 518 def rmenu_check_copy(self): 519 try: 520 indx = self.text.index('sel.first') 521 except TclError: 522 return 'disabled' 523 else: 524 return 'normal' if indx else 'disabled' 525 526 def rmenu_check_paste(self): 527 try: 528 self.text.tk.call('tk::GetSelection', self.text, 'CLIPBOARD') 529 except TclError: 530 return 'disabled' 531 else: 532 return 'normal' 533 534 def about_dialog(self, event=None): 535 aboutDialog.AboutDialog(self.top,'About IDLE') 536 537 def config_dialog(self, event=None): 538 configDialog.ConfigDialog(self.top,'Settings') 539 540 def help_dialog(self, event=None): 541 if self.root: 542 parent = self.root 543 else: 544 parent = self.top 545 helpDialog.display(parent, near=self.top) 546 547 def python_docs(self, event=None): 548 if sys.platform[:3] == 'win': 549 try: 550 os.startfile(self.help_url) 551 except WindowsError as why: 552 tkMessageBox.showerror(title='Document Start Failure', 553 message=str(why), parent=self.text) 554 else: 555 webbrowser.open(self.help_url) 556 return "break" 557 558 def cut(self,event): 559 self.text.event_generate("<<Cut>>") 560 return "break" 561 562 def copy(self,event): 563 if not self.text.tag_ranges("sel"): 564 # There is no selection, so do nothing and maybe interrupt. 565 return 566 self.text.event_generate("<<Copy>>") 567 return "break" 568 569 def paste(self,event): 570 self.text.event_generate("<<Paste>>") 571 self.text.see("insert") 572 return "break" 573 574 def select_all(self, event=None): 575 self.text.tag_add("sel", "1.0", "end-1c") 576 self.text.mark_set("insert", "1.0") 577 self.text.see("insert") 578 return "break" 579 580 def remove_selection(self, event=None): 581 self.text.tag_remove("sel", "1.0", "end") 582 self.text.see("insert") 583 584 def move_at_edge_if_selection(self, edge_index): 585 """Cursor move begins at start or end of selection 586 587 When a left/right cursor key is pressed create and return to Tkinter a 588 function which causes a cursor move from the associated edge of the 589 selection. 590 591 """ 592 self_text_index = self.text.index 593 self_text_mark_set = self.text.mark_set 594 edges_table = ("sel.first+1c", "sel.last-1c") 595 def move_at_edge(event): 596 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed 597 try: 598 self_text_index("sel.first") 599 self_text_mark_set("insert", edges_table[edge_index]) 600 except TclError: 601 pass 602 return move_at_edge 603 604 def del_word_left(self, event): 605 self.text.event_generate('<Meta-Delete>') 606 return "break" 607 608 def del_word_right(self, event): 609 self.text.event_generate('<Meta-d>') 610 return "break" 611 612 def find_event(self, event): 613 SearchDialog.find(self.text) 614 return "break" 615 616 def find_again_event(self, event): 617 SearchDialog.find_again(self.text) 618 return "break" 619 620 def find_selection_event(self, event): 621 SearchDialog.find_selection(self.text) 622 return "break" 623 624 def find_in_files_event(self, event): 625 GrepDialog.grep(self.text, self.io, self.flist) 626 return "break" 627 628 def replace_event(self, event): 629 ReplaceDialog.replace(self.text) 630 return "break" 631 632 def goto_line_event(self, event): 633 text = self.text 634 lineno = tkSimpleDialog.askinteger("Goto", 635 "Go to line number:",parent=text) 636 if lineno is None: 637 return "break" 638 if lineno <= 0: 639 text.bell() 640 return "break" 641 text.mark_set("insert", "%d.0" % lineno) 642 text.see("insert") 643 644 def open_module(self, event=None): 645 # XXX Shouldn't this be in IOBinding or in FileList? 646 try: 647 name = self.text.get("sel.first", "sel.last") 648 except TclError: 649 name = "" 650 else: 651 name = name.strip() 652 name = tkSimpleDialog.askstring("Module", 653 "Enter the name of a Python module\n" 654 "to search on sys.path and open:", 655 parent=self.text, initialvalue=name) 656 if name: 657 name = name.strip() 658 if not name: 659 return 660 # XXX Ought to insert current file's directory in front of path 661 try: 662 (f, file, (suffix, mode, type)) = _find_module(name) 663 except (NameError, ImportError), msg: 664 tkMessageBox.showerror("Import error", str(msg), parent=self.text) 665 return 666 if type != imp.PY_SOURCE: 667 tkMessageBox.showerror("Unsupported type", 668 "%s is not a source module" % name, parent=self.text) 669 return 670 if f: 671 f.close() 672 if self.flist: 673 self.flist.open(file) 674 else: 675 self.io.loadfile(file) 676 677 def open_class_browser(self, event=None): 678 filename = self.io.filename 679 if not filename: 680 tkMessageBox.showerror( 681 "No filename", 682 "This buffer has no associated filename", 683 master=self.text) 684 self.text.focus_set() 685 return None 686 head, tail = os.path.split(filename) 687 base, ext = os.path.splitext(tail) 688 from idlelib import ClassBrowser 689 ClassBrowser.ClassBrowser(self.flist, base, [head]) 690 691 def open_path_browser(self, event=None): 692 from idlelib import PathBrowser 693 PathBrowser.PathBrowser(self.flist) 694 695 def gotoline(self, lineno): 696 if lineno is not None and lineno > 0: 697 self.text.mark_set("insert", "%d.0" % lineno) 698 self.text.tag_remove("sel", "1.0", "end") 699 self.text.tag_add("sel", "insert", "insert +1l") 700 self.center() 701 702 def ispythonsource(self, filename): 703 if not filename or os.path.isdir(filename): 704 return True 705 base, ext = os.path.splitext(os.path.basename(filename)) 706 if os.path.normcase(ext) in (".py", ".pyw"): 707 return True 708 try: 709 f = open(filename) 710 line = f.readline() 711 f.close() 712 except IOError: 713 return False 714 return line.startswith('#!') and line.find('python') >= 0 715 716 def close_hook(self): 717 if self.flist: 718 self.flist.unregister_maybe_terminate(self) 719 self.flist = None 720 721 def set_close_hook(self, close_hook): 722 self.close_hook = close_hook 723 724 def filename_change_hook(self): 725 if self.flist: 726 self.flist.filename_changed_edit(self) 727 self.saved_change_hook() 728 self.top.update_windowlist_registry(self) 729 self.ResetColorizer() 730 731 def _addcolorizer(self): 732 if self.color: 733 return 734 if self.ispythonsource(self.io.filename): 735 self.color = self.ColorDelegator() 736 # can add more colorizers here... 737 if self.color: 738 self.per.removefilter(self.undo) 739 self.per.insertfilter(self.color) 740 self.per.insertfilter(self.undo) 741 742 def _rmcolorizer(self): 743 if not self.color: 744 return 745 self.color.removecolors() 746 self.per.removefilter(self.color) 747 self.color = None 748 749 def ResetColorizer(self): 750 "Update the colour theme" 751 # Called from self.filename_change_hook and from configDialog.py 752 self._rmcolorizer() 753 self._addcolorizer() 754 theme = idleConf.GetOption('main','Theme','name') 755 normal_colors = idleConf.GetHighlight(theme, 'normal') 756 cursor_color = idleConf.GetHighlight(theme, 'cursor', fgBg='fg') 757 select_colors = idleConf.GetHighlight(theme, 'hilite') 758 self.text.config( 759 foreground=normal_colors['foreground'], 760 background=normal_colors['background'], 761 insertbackground=cursor_color, 762 selectforeground=select_colors['foreground'], 763 selectbackground=select_colors['background'], 764 ) 765 766 def ResetFont(self): 767 "Update the text widgets' font if it is changed" 768 # Called from configDialog.py 769 fontWeight='normal' 770 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'): 771 fontWeight='bold' 772 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'), 773 idleConf.GetOption('main','EditorWindow','font-size', 774 type='int'), 775 fontWeight)) 776 777 def RemoveKeybindings(self): 778 "Remove the keybindings before they are changed." 779 # Called from configDialog.py 780 self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 781 for event, keylist in keydefs.items(): 782 self.text.event_delete(event, *keylist) 783 for extensionName in self.get_standard_extension_names(): 784 xkeydefs = idleConf.GetExtensionBindings(extensionName) 785 if xkeydefs: 786 for event, keylist in xkeydefs.items(): 787 self.text.event_delete(event, *keylist) 788 789 def ApplyKeybindings(self): 790 "Update the keybindings after they are changed" 791 # Called from configDialog.py 792 self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet() 793 self.apply_bindings() 794 for extensionName in self.get_standard_extension_names(): 795 xkeydefs = idleConf.GetExtensionBindings(extensionName) 796 if xkeydefs: 797 self.apply_bindings(xkeydefs) 798 #update menu accelerators 799 menuEventDict = {} 800 for menu in self.Bindings.menudefs: 801 menuEventDict[menu[0]] = {} 802 for item in menu[1]: 803 if item: 804 menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1] 805 for menubarItem in self.menudict.keys(): 806 menu = self.menudict[menubarItem] 807 end = menu.index(END) + 1 808 for index in range(0, end): 809 if menu.type(index) == 'command': 810 accel = menu.entrycget(index, 'accelerator') 811 if accel: 812 itemName = menu.entrycget(index, 'label') 813 event = '' 814 if menubarItem in menuEventDict: 815 if itemName in menuEventDict[menubarItem]: 816 event = menuEventDict[menubarItem][itemName] 817 if event: 818 accel = get_accelerator(keydefs, event) 819 menu.entryconfig(index, accelerator=accel) 820 821 def set_notabs_indentwidth(self): 822 "Update the indentwidth if changed and not using tabs in this window" 823 # Called from configDialog.py 824 if not self.usetabs: 825 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces', 826 type='int') 827 828 def reset_help_menu_entries(self): 829 "Update the additional help entries on the Help menu" 830 help_list = idleConf.GetAllExtraHelpSourcesList() 831 helpmenu = self.menudict['help'] 832 # first delete the extra help entries, if any 833 helpmenu_length = helpmenu.index(END) 834 if helpmenu_length > self.base_helpmenu_length: 835 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length) 836 # then rebuild them 837 if help_list: 838 helpmenu.add_separator() 839 for entry in help_list: 840 cmd = self.__extra_help_callback(entry[1]) 841 helpmenu.add_command(label=entry[0], command=cmd) 842 # and update the menu dictionary 843 self.menudict['help'] = helpmenu 844 845 def __extra_help_callback(self, helpfile): 846 "Create a callback with the helpfile value frozen at definition time" 847 def display_extra_help(helpfile=helpfile): 848 if not helpfile.startswith(('www', 'http')): 849 helpfile = os.path.normpath(helpfile) 850 if sys.platform[:3] == 'win': 851 try: 852 os.startfile(helpfile) 853 except WindowsError as why: 854 tkMessageBox.showerror(title='Document Start Failure', 855 message=str(why), parent=self.text) 856 else: 857 webbrowser.open(helpfile) 858 return display_extra_help 859 860 def update_recent_files_list(self, new_file=None): 861 "Load and update the recent files list and menus" 862 rf_list = [] 863 if os.path.exists(self.recent_files_path): 864 rf_list_file = open(self.recent_files_path,'r') 865 try: 866 rf_list = rf_list_file.readlines() 867 finally: 868 rf_list_file.close() 869 if new_file: 870 new_file = os.path.abspath(new_file) + '\n' 871 if new_file in rf_list: 872 rf_list.remove(new_file) # move to top 873 rf_list.insert(0, new_file) 874 # clean and save the recent files list 875 bad_paths = [] 876 for path in rf_list: 877 if '\0' in path or not os.path.exists(path[0:-1]): 878 bad_paths.append(path) 879 rf_list = [path for path in rf_list if path not in bad_paths] 880 ulchars = "1234567890ABCDEFGHIJK" 881 rf_list = rf_list[0:len(ulchars)] 882 try: 883 with open(self.recent_files_path, 'w') as rf_file: 884 rf_file.writelines(rf_list) 885 except IOError as err: 886 if not getattr(self.root, "recentfilelist_error_displayed", False): 887 self.root.recentfilelist_error_displayed = True 888 tkMessageBox.showerror(title='IDLE Error', 889 message='Unable to update Recent Files list:\n%s' 890 % str(err), 891 parent=self.text) 892 # for each edit window instance, construct the recent files menu 893 for instance in self.top.instance_dict.keys(): 894 menu = instance.recent_files_menu 895 menu.delete(0, END) # clear, and rebuild: 896 for i, file_name in enumerate(rf_list): 897 file_name = file_name.rstrip() # zap \n 898 # make unicode string to display non-ASCII chars correctly 899 ufile_name = self._filename_to_unicode(file_name) 900 callback = instance.__recent_file_callback(file_name) 901 menu.add_command(label=ulchars[i] + " " + ufile_name, 902 command=callback, 903 underline=0) 904 905 def __recent_file_callback(self, file_name): 906 def open_recent_file(fn_closure=file_name): 907 self.io.open(editFile=fn_closure) 908 return open_recent_file 909 910 def saved_change_hook(self): 911 short = self.short_title() 912 long = self.long_title() 913 if short and long: 914 title = short + " - " + long 915 elif short: 916 title = short 917 elif long: 918 title = long 919 else: 920 title = "Untitled" 921 icon = short or long or title 922 if not self.get_saved(): 923 title = "*%s*" % title 924 icon = "*%s" % icon 925 self.top.wm_title(title) 926 self.top.wm_iconname(icon) 927 928 def get_saved(self): 929 return self.undo.get_saved() 930 931 def set_saved(self, flag): 932 self.undo.set_saved(flag) 933 934 def reset_undo(self): 935 self.undo.reset_undo() 936 937 def short_title(self): 938 filename = self.io.filename 939 if filename: 940 filename = os.path.basename(filename) 941 # return unicode string to display non-ASCII chars correctly 942 return self._filename_to_unicode(filename) 943 944 def long_title(self): 945 # return unicode string to display non-ASCII chars correctly 946 return self._filename_to_unicode(self.io.filename or "") 947 948 def center_insert_event(self, event): 949 self.center() 950 951 def center(self, mark="insert"): 952 text = self.text 953 top, bot = self.getwindowlines() 954 lineno = self.getlineno(mark) 955 height = bot - top 956 newtop = max(1, lineno - height//2) 957 text.yview(float(newtop)) 958 959 def getwindowlines(self): 960 text = self.text 961 top = self.getlineno("@0,0") 962 bot = self.getlineno("@0,65535") 963 if top == bot and text.winfo_height() == 1: 964 # Geometry manager hasn't run yet 965 height = int(text['height']) 966 bot = top + height - 1 967 return top, bot 968 969 def getlineno(self, mark="insert"): 970 text = self.text 971 return int(float(text.index(mark))) 972 973 def get_geometry(self): 974 "Return (width, height, x, y)" 975 geom = self.top.wm_geometry() 976 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom) 977 tuple = (map(int, m.groups())) 978 return tuple 979 980 def close_event(self, event): 981 self.close() 982 983 def maybesave(self): 984 if self.io: 985 if not self.get_saved(): 986 if self.top.state()!='normal': 987 self.top.deiconify() 988 self.top.lower() 989 self.top.lift() 990 return self.io.maybesave() 991 992 def close(self): 993 reply = self.maybesave() 994 if str(reply) != "cancel": 995 self._close() 996 return reply 997 998 def _close(self): 999 if self.io.filename: 1000 self.update_recent_files_list(new_file=self.io.filename) 1001 WindowList.unregister_callback(self.postwindowsmenu) 1002 self.unload_extensions() 1003 self.io.close() 1004 self.io = None 1005 self.undo = None 1006 if self.color: 1007 self.color.close(False) 1008 self.color = None 1009 self.text = None 1010 self.tkinter_vars = None 1011 self.per.close() 1012 self.per = None 1013 self.top.destroy() 1014 if self.close_hook: 1015 # unless override: unregister from flist, terminate if last window 1016 self.close_hook() 1017 1018 def load_extensions(self): 1019 self.extensions = {} 1020 self.load_standard_extensions() 1021 1022 def unload_extensions(self): 1023 for ins in self.extensions.values(): 1024 if hasattr(ins, "close"): 1025 ins.close() 1026 self.extensions = {} 1027 1028 def load_standard_extensions(self): 1029 for name in self.get_standard_extension_names(): 1030 try: 1031 self.load_extension(name) 1032 except: 1033 print "Failed to load extension", repr(name) 1034 import traceback 1035 traceback.print_exc() 1036 1037 def get_standard_extension_names(self): 1038 return idleConf.GetExtensions(editor_only=True) 1039 1040 def load_extension(self, name): 1041 try: 1042 mod = __import__(name, globals(), locals(), []) 1043 except ImportError: 1044 print "\nFailed to import extension: ", name 1045 return 1046 cls = getattr(mod, name) 1047 keydefs = idleConf.GetExtensionBindings(name) 1048 if hasattr(cls, "menudefs"): 1049 self.fill_menus(cls.menudefs, keydefs) 1050 ins = cls(self) 1051 self.extensions[name] = ins 1052 if keydefs: 1053 self.apply_bindings(keydefs) 1054 for vevent in keydefs.keys(): 1055 methodname = vevent.replace("-", "_") 1056 while methodname[:1] == '<': 1057 methodname = methodname[1:] 1058 while methodname[-1:] == '>': 1059 methodname = methodname[:-1] 1060 methodname = methodname + "_event" 1061 if hasattr(ins, methodname): 1062 self.text.bind(vevent, getattr(ins, methodname)) 1063 1064 def apply_bindings(self, keydefs=None): 1065 if keydefs is None: 1066 keydefs = self.Bindings.default_keydefs 1067 text = self.text 1068 text.keydefs = keydefs 1069 for event, keylist in keydefs.items(): 1070 if keylist: 1071 text.event_add(event, *keylist) 1072 1073 def fill_menus(self, menudefs=None, keydefs=None): 1074 """Add appropriate entries to the menus and submenus 1075 1076 Menus that are absent or None in self.menudict are ignored. 1077 """ 1078 if menudefs is None: 1079 menudefs = self.Bindings.menudefs 1080 if keydefs is None: 1081 keydefs = self.Bindings.default_keydefs 1082 menudict = self.menudict 1083 text = self.text 1084 for mname, entrylist in menudefs: 1085 menu = menudict.get(mname) 1086 if not menu: 1087 continue 1088 for entry in entrylist: 1089 if not entry: 1090 menu.add_separator() 1091 else: 1092 label, eventname = entry 1093 checkbutton = (label[:1] == '!') 1094 if checkbutton: 1095 label = label[1:] 1096 underline, label = prepstr(label) 1097 accelerator = get_accelerator(keydefs, eventname) 1098 def command(text=text, eventname=eventname): 1099 text.event_generate(eventname) 1100 if checkbutton: 1101 var = self.get_var_obj(eventname, BooleanVar) 1102 menu.add_checkbutton(label=label, underline=underline, 1103 command=command, accelerator=accelerator, 1104 variable=var) 1105 else: 1106 menu.add_command(label=label, underline=underline, 1107 command=command, 1108 accelerator=accelerator) 1109 1110 def getvar(self, name): 1111 var = self.get_var_obj(name) 1112 if var: 1113 value = var.get() 1114 return value 1115 else: 1116 raise NameError, name 1117 1118 def setvar(self, name, value, vartype=None): 1119 var = self.get_var_obj(name, vartype) 1120 if var: 1121 var.set(value) 1122 else: 1123 raise NameError, name 1124 1125 def get_var_obj(self, name, vartype=None): 1126 var = self.tkinter_vars.get(name) 1127 if not var and vartype: 1128 # create a Tkinter variable object with self.text as master: 1129 self.tkinter_vars[name] = var = vartype(self.text) 1130 return var 1131 1132 # Tk implementations of "virtual text methods" -- each platform 1133 # reusing IDLE's support code needs to define these for its GUI's 1134 # flavor of widget. 1135 1136 # Is character at text_index in a Python string? Return 0 for 1137 # "guaranteed no", true for anything else. This info is expensive 1138 # to compute ab initio, but is probably already known by the 1139 # platform's colorizer. 1140 1141 def is_char_in_string(self, text_index): 1142 if self.color: 1143 # Return true iff colorizer hasn't (re)gotten this far 1144 # yet, or the character is tagged as being in a string 1145 return self.text.tag_prevrange("TODO", text_index) or \ 1146 "STRING" in self.text.tag_names(text_index) 1147 else: 1148 # The colorizer is missing: assume the worst 1149 return 1 1150 1151 # If a selection is defined in the text widget, return (start, 1152 # end) as Tkinter text indices, otherwise return (None, None) 1153 def get_selection_indices(self): 1154 try: 1155 first = self.text.index("sel.first") 1156 last = self.text.index("sel.last") 1157 return first, last 1158 except TclError: 1159 return None, None 1160 1161 # Return the text widget's current view of what a tab stop means 1162 # (equivalent width in spaces). 1163 1164 def get_tabwidth(self): 1165 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT 1166 return int(current) 1167 1168 # Set the text widget's current view of what a tab stop means. 1169 1170 def set_tabwidth(self, newtabwidth): 1171 text = self.text 1172 if self.get_tabwidth() != newtabwidth: 1173 pixels = text.tk.call("font", "measure", text["font"], 1174 "-displayof", text.master, 1175 "n" * newtabwidth) 1176 text.configure(tabs=pixels) 1177 1178 # If ispythonsource and guess are true, guess a good value for 1179 # indentwidth based on file content (if possible), and if 1180 # indentwidth != tabwidth set usetabs false. 1181 # In any case, adjust the Text widget's view of what a tab 1182 # character means. 1183 1184 def set_indentation_params(self, ispythonsource, guess=True): 1185 if guess and ispythonsource: 1186 i = self.guess_indent() 1187 if 2 <= i <= 8: 1188 self.indentwidth = i 1189 if self.indentwidth != self.tabwidth: 1190 self.usetabs = False 1191 self.set_tabwidth(self.tabwidth) 1192 1193 def smart_backspace_event(self, event): 1194 text = self.text 1195 first, last = self.get_selection_indices() 1196 if first and last: 1197 text.delete(first, last) 1198 text.mark_set("insert", first) 1199 return "break" 1200 # Delete whitespace left, until hitting a real char or closest 1201 # preceding virtual tab stop. 1202 chars = text.get("insert linestart", "insert") 1203 if chars == '': 1204 if text.compare("insert", ">", "1.0"): 1205 # easy: delete preceding newline 1206 text.delete("insert-1c") 1207 else: 1208 text.bell() # at start of buffer 1209 return "break" 1210 if chars[-1] not in " \t": 1211 # easy: delete preceding real char 1212 text.delete("insert-1c") 1213 return "break" 1214 # Ick. It may require *inserting* spaces if we back up over a 1215 # tab character! This is written to be clear, not fast. 1216 tabwidth = self.tabwidth 1217 have = len(chars.expandtabs(tabwidth)) 1218 assert have > 0 1219 want = ((have - 1) // self.indentwidth) * self.indentwidth 1220 # Debug prompt is multilined.... 1221 if self.context_use_ps1: 1222 last_line_of_prompt = sys.ps1.split('\n')[-1] 1223 else: 1224 last_line_of_prompt = '' 1225 ncharsdeleted = 0 1226 while 1: 1227 if chars == last_line_of_prompt: 1228 break 1229 chars = chars[:-1] 1230 ncharsdeleted = ncharsdeleted + 1 1231 have = len(chars.expandtabs(tabwidth)) 1232 if have <= want or chars[-1] not in " \t": 1233 break 1234 text.undo_block_start() 1235 text.delete("insert-%dc" % ncharsdeleted, "insert") 1236 if have < want: 1237 text.insert("insert", ' ' * (want - have)) 1238 text.undo_block_stop() 1239 return "break" 1240 1241 def smart_indent_event(self, event): 1242 # if intraline selection: 1243 # delete it 1244 # elif multiline selection: 1245 # do indent-region 1246 # else: 1247 # indent one level 1248 text = self.text 1249 first, last = self.get_selection_indices() 1250 text.undo_block_start() 1251 try: 1252 if first and last: 1253 if index2line(first) != index2line(last): 1254 return self.indent_region_event(event) 1255 text.delete(first, last) 1256 text.mark_set("insert", first) 1257 prefix = text.get("insert linestart", "insert") 1258 raw, effective = classifyws(prefix, self.tabwidth) 1259 if raw == len(prefix): 1260 # only whitespace to the left 1261 self.reindent_to(effective + self.indentwidth) 1262 else: 1263 # tab to the next 'stop' within or to right of line's text: 1264 if self.usetabs: 1265 pad = '\t' 1266 else: 1267 effective = len(prefix.expandtabs(self.tabwidth)) 1268 n = self.indentwidth 1269 pad = ' ' * (n - effective % n) 1270 text.insert("insert", pad) 1271 text.see("insert") 1272 return "break" 1273 finally: 1274 text.undo_block_stop() 1275 1276 def newline_and_indent_event(self, event): 1277 text = self.text 1278 first, last = self.get_selection_indices() 1279 text.undo_block_start() 1280 try: 1281 if first and last: 1282 text.delete(first, last) 1283 text.mark_set("insert", first) 1284 line = text.get("insert linestart", "insert") 1285 i, n = 0, len(line) 1286 while i < n and line[i] in " \t": 1287 i = i+1 1288 if i == n: 1289 # the cursor is in or at leading indentation in a continuation 1290 # line; just inject an empty line at the start 1291 text.insert("insert linestart", '\n') 1292 return "break" 1293 indent = line[:i] 1294 # strip whitespace before insert point unless it's in the prompt 1295 i = 0 1296 last_line_of_prompt = sys.ps1.split('\n')[-1] 1297 while line and line[-1] in " \t" and line != last_line_of_prompt: 1298 line = line[:-1] 1299 i = i+1 1300 if i: 1301 text.delete("insert - %d chars" % i, "insert") 1302 # strip whitespace after insert point 1303 while text.get("insert") in " \t": 1304 text.delete("insert") 1305 # start new line 1306 text.insert("insert", '\n') 1307 1308 # adjust indentation for continuations and block 1309 # open/close first need to find the last stmt 1310 lno = index2line(text.index('insert')) 1311 y = PyParse.Parser(self.indentwidth, self.tabwidth) 1312 if not self.context_use_ps1: 1313 for context in self.num_context_lines: 1314 startat = max(lno - context, 1) 1315 startatindex = repr(startat) + ".0" 1316 rawtext = text.get(startatindex, "insert") 1317 y.set_str(rawtext) 1318 bod = y.find_good_parse_start( 1319 self.context_use_ps1, 1320 self._build_char_in_string_func(startatindex)) 1321 if bod is not None or startat == 1: 1322 break 1323 y.set_lo(bod or 0) 1324 else: 1325 r = text.tag_prevrange("console", "insert") 1326 if r: 1327 startatindex = r[1] 1328 else: 1329 startatindex = "1.0" 1330 rawtext = text.get(startatindex, "insert") 1331 y.set_str(rawtext) 1332 y.set_lo(0) 1333 1334 c = y.get_continuation_type() 1335 if c != PyParse.C_NONE: 1336 # The current stmt hasn't ended yet. 1337 if c == PyParse.C_STRING_FIRST_LINE: 1338 # after the first line of a string; do not indent at all 1339 pass 1340 elif c == PyParse.C_STRING_NEXT_LINES: 1341 # inside a string which started before this line; 1342 # just mimic the current indent 1343 text.insert("insert", indent) 1344 elif c == PyParse.C_BRACKET: 1345 # line up with the first (if any) element of the 1346 # last open bracket structure; else indent one 1347 # level beyond the indent of the line with the 1348 # last open bracket 1349 self.reindent_to(y.compute_bracket_indent()) 1350 elif c == PyParse.C_BACKSLASH: 1351 # if more than one line in this stmt already, just 1352 # mimic the current indent; else if initial line 1353 # has a start on an assignment stmt, indent to 1354 # beyond leftmost =; else to beyond first chunk of 1355 # non-whitespace on initial line 1356 if y.get_num_lines_in_stmt() > 1: 1357 text.insert("insert", indent) 1358 else: 1359 self.reindent_to(y.compute_backslash_indent()) 1360 else: 1361 assert 0, "bogus continuation type %r" % (c,) 1362 return "break" 1363 1364 # This line starts a brand new stmt; indent relative to 1365 # indentation of initial line of closest preceding 1366 # interesting stmt. 1367 indent = y.get_base_indent_string() 1368 text.insert("insert", indent) 1369 if y.is_block_opener(): 1370 self.smart_indent_event(event) 1371 elif indent and y.is_block_closer(): 1372 self.smart_backspace_event(event) 1373 return "break" 1374 finally: 1375 text.see("insert") 1376 text.undo_block_stop() 1377 1378 # Our editwin provides a is_char_in_string function that works 1379 # with a Tk text index, but PyParse only knows about offsets into 1380 # a string. This builds a function for PyParse that accepts an 1381 # offset. 1382 1383 def _build_char_in_string_func(self, startindex): 1384 def inner(offset, _startindex=startindex, 1385 _icis=self.is_char_in_string): 1386 return _icis(_startindex + "+%dc" % offset) 1387 return inner 1388 1389 def indent_region_event(self, event): 1390 head, tail, chars, lines = self.get_region() 1391 for pos in range(len(lines)): 1392 line = lines[pos] 1393 if line: 1394 raw, effective = classifyws(line, self.tabwidth) 1395 effective = effective + self.indentwidth 1396 lines[pos] = self._make_blanks(effective) + line[raw:] 1397 self.set_region(head, tail, chars, lines) 1398 return "break" 1399 1400 def dedent_region_event(self, event): 1401 head, tail, chars, lines = self.get_region() 1402 for pos in range(len(lines)): 1403 line = lines[pos] 1404 if line: 1405 raw, effective = classifyws(line, self.tabwidth) 1406 effective = max(effective - self.indentwidth, 0) 1407 lines[pos] = self._make_blanks(effective) + line[raw:] 1408 self.set_region(head, tail, chars, lines) 1409 return "break" 1410 1411 def comment_region_event(self, event): 1412 head, tail, chars, lines = self.get_region() 1413 for pos in range(len(lines) - 1): 1414 line = lines[pos] 1415 lines[pos] = '##' + line 1416 self.set_region(head, tail, chars, lines) 1417 1418 def uncomment_region_event(self, event): 1419 head, tail, chars, lines = self.get_region() 1420 for pos in range(len(lines)): 1421 line = lines[pos] 1422 if not line: 1423 continue 1424 if line[:2] == '##': 1425 line = line[2:] 1426 elif line[:1] == '#': 1427 line = line[1:] 1428 lines[pos] = line 1429 self.set_region(head, tail, chars, lines) 1430 1431 def tabify_region_event(self, event): 1432 head, tail, chars, lines = self.get_region() 1433 tabwidth = self._asktabwidth() 1434 if tabwidth is None: return 1435 for pos in range(len(lines)): 1436 line = lines[pos] 1437 if line: 1438 raw, effective = classifyws(line, tabwidth) 1439 ntabs, nspaces = divmod(effective, tabwidth) 1440 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:] 1441 self.set_region(head, tail, chars, lines) 1442 1443 def untabify_region_event(self, event): 1444 head, tail, chars, lines = self.get_region() 1445 tabwidth = self._asktabwidth() 1446 if tabwidth is None: return 1447 for pos in range(len(lines)): 1448 lines[pos] = lines[pos].expandtabs(tabwidth) 1449 self.set_region(head, tail, chars, lines) 1450 1451 def toggle_tabs_event(self, event): 1452 if self.askyesno( 1453 "Toggle tabs", 1454 "Turn tabs " + ("on", "off")[self.usetabs] + 1455 "?\nIndent width " + 1456 ("will be", "remains at")[self.usetabs] + " 8." + 1457 "\n Note: a tab is always 8 columns", 1458 parent=self.text): 1459 self.usetabs = not self.usetabs 1460 # Try to prevent inconsistent indentation. 1461 # User must change indent width manually after using tabs. 1462 self.indentwidth = 8 1463 return "break" 1464 1465 # XXX this isn't bound to anything -- see tabwidth comments 1466## def change_tabwidth_event(self, event): 1467## new = self._asktabwidth() 1468## if new != self.tabwidth: 1469## self.tabwidth = new 1470## self.set_indentation_params(0, guess=0) 1471## return "break" 1472 1473 def change_indentwidth_event(self, event): 1474 new = self.askinteger( 1475 "Indent width", 1476 "New indent width (2-16)\n(Always use 8 when using tabs)", 1477 parent=self.text, 1478 initialvalue=self.indentwidth, 1479 minvalue=2, 1480 maxvalue=16) 1481 if new and new != self.indentwidth and not self.usetabs: 1482 self.indentwidth = new 1483 return "break" 1484 1485 def get_region(self): 1486 text = self.text 1487 first, last = self.get_selection_indices() 1488 if first and last: 1489 head = text.index(first + " linestart") 1490 tail = text.index(last + "-1c lineend +1c") 1491 else: 1492 head = text.index("insert linestart") 1493 tail = text.index("insert lineend +1c") 1494 chars = text.get(head, tail) 1495 lines = chars.split("\n") 1496 return head, tail, chars, lines 1497 1498 def set_region(self, head, tail, chars, lines): 1499 text = self.text 1500 newchars = "\n".join(lines) 1501 if newchars == chars: 1502 text.bell() 1503 return 1504 text.tag_remove("sel", "1.0", "end") 1505 text.mark_set("insert", head) 1506 text.undo_block_start() 1507 text.delete(head, tail) 1508 text.insert(head, newchars) 1509 text.undo_block_stop() 1510 text.tag_add("sel", head, "insert") 1511 1512 # Make string that displays as n leading blanks. 1513 1514 def _make_blanks(self, n): 1515 if self.usetabs: 1516 ntabs, nspaces = divmod(n, self.tabwidth) 1517 return '\t' * ntabs + ' ' * nspaces 1518 else: 1519 return ' ' * n 1520 1521 # Delete from beginning of line to insert point, then reinsert 1522 # column logical (meaning use tabs if appropriate) spaces. 1523 1524 def reindent_to(self, column): 1525 text = self.text 1526 text.undo_block_start() 1527 if text.compare("insert linestart", "!=", "insert"): 1528 text.delete("insert linestart", "insert") 1529 if column: 1530 text.insert("insert", self._make_blanks(column)) 1531 text.undo_block_stop() 1532 1533 def _asktabwidth(self): 1534 return self.askinteger( 1535 "Tab width", 1536 "Columns per tab? (2-16)", 1537 parent=self.text, 1538 initialvalue=self.indentwidth, 1539 minvalue=2, 1540 maxvalue=16) 1541 1542 # Guess indentwidth from text content. 1543 # Return guessed indentwidth. This should not be believed unless 1544 # it's in a reasonable range (e.g., it will be 0 if no indented 1545 # blocks are found). 1546 1547 def guess_indent(self): 1548 opener, indented = IndentSearcher(self.text, self.tabwidth).run() 1549 if opener and indented: 1550 raw, indentsmall = classifyws(opener, self.tabwidth) 1551 raw, indentlarge = classifyws(indented, self.tabwidth) 1552 else: 1553 indentsmall = indentlarge = 0 1554 return indentlarge - indentsmall 1555 1556# "line.col" -> line, as an int 1557def index2line(index): 1558 return int(float(index)) 1559 1560# Look at the leading whitespace in s. 1561# Return pair (# of leading ws characters, 1562# effective # of leading blanks after expanding 1563# tabs to width tabwidth) 1564 1565def classifyws(s, tabwidth): 1566 raw = effective = 0 1567 for ch in s: 1568 if ch == ' ': 1569 raw = raw + 1 1570 effective = effective + 1 1571 elif ch == '\t': 1572 raw = raw + 1 1573 effective = (effective // tabwidth + 1) * tabwidth 1574 else: 1575 break 1576 return raw, effective 1577 1578import tokenize 1579_tokenize = tokenize 1580del tokenize 1581 1582class IndentSearcher(object): 1583 1584 # .run() chews over the Text widget, looking for a block opener 1585 # and the stmt following it. Returns a pair, 1586 # (line containing block opener, line containing stmt) 1587 # Either or both may be None. 1588 1589 def __init__(self, text, tabwidth): 1590 self.text = text 1591 self.tabwidth = tabwidth 1592 self.i = self.finished = 0 1593 self.blkopenline = self.indentedline = None 1594 1595 def readline(self): 1596 if self.finished: 1597 return "" 1598 i = self.i = self.i + 1 1599 mark = repr(i) + ".0" 1600 if self.text.compare(mark, ">=", "end"): 1601 return "" 1602 return self.text.get(mark, mark + " lineend+1c") 1603 1604 def tokeneater(self, type, token, start, end, line, 1605 INDENT=_tokenize.INDENT, 1606 NAME=_tokenize.NAME, 1607 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')): 1608 if self.finished: 1609 pass 1610 elif type == NAME and token in OPENERS: 1611 self.blkopenline = line 1612 elif type == INDENT and self.blkopenline: 1613 self.indentedline = line 1614 self.finished = 1 1615 1616 def run(self): 1617 save_tabsize = _tokenize.tabsize 1618 _tokenize.tabsize = self.tabwidth 1619 try: 1620 try: 1621 _tokenize.tokenize(self.readline, self.tokeneater) 1622 except (_tokenize.TokenError, SyntaxError): 1623 # since we cut off the tokenizer early, we can trigger 1624 # spurious errors 1625 pass 1626 finally: 1627 _tokenize.tabsize = save_tabsize 1628 return self.blkopenline, self.indentedline 1629 1630### end autoindent code ### 1631 1632def prepstr(s): 1633 # Helper to extract the underscore from a string, e.g. 1634 # prepstr("Co_py") returns (2, "Copy"). 1635 i = s.find('_') 1636 if i >= 0: 1637 s = s[:i] + s[i+1:] 1638 return i, s 1639 1640 1641keynames = { 1642 'bracketleft': '[', 1643 'bracketright': ']', 1644 'slash': '/', 1645} 1646 1647def get_accelerator(keydefs, eventname): 1648 keylist = keydefs.get(eventname) 1649 # issue10940: temporary workaround to prevent hang with OS X Cocoa Tk 8.5 1650 # if not keylist: 1651 if (not keylist) or (macosxSupport.runningAsOSXApp() and eventname in { 1652 "<<open-module>>", 1653 "<<goto-line>>", 1654 "<<change-indentwidth>>"}): 1655 return "" 1656 s = keylist[0] 1657 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s) 1658 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s) 1659 s = re.sub("Key-", "", s) 1660 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu 1661 s = re.sub("Control-", "Ctrl-", s) 1662 s = re.sub("-", "+", s) 1663 s = re.sub("><", " ", s) 1664 s = re.sub("<", "", s) 1665 s = re.sub(">", "", s) 1666 return s 1667 1668 1669def fixwordbreaks(root): 1670 # Make sure that Tk's double-click and next/previous word 1671 # operations use our definition of a word (i.e. an identifier) 1672 tk = root.tk 1673 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded 1674 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]') 1675 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]') 1676 1677 1678def test(): 1679 root = Tk() 1680 fixwordbreaks(root) 1681 root.withdraw() 1682 if sys.argv[1:]: 1683 filename = sys.argv[1] 1684 else: 1685 filename = None 1686 edit = EditorWindow(root=root, filename=filename) 1687 edit.set_close_hook(root.quit) 1688 edit.text.bind("<<close-all-windows>>", edit.close_event) 1689 root.mainloop() 1690 root.destroy() 1691 1692if __name__ == '__main__': 1693 test() 1694