blob: 0f1466287bf266a23bcbedebd2ff5a2ec5d72268 [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001import sys
2import os
David Scherer7aced172000-08-15 01:13:23 +00003import re
4import imp
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +00005from itertools import count
David Scherer7aced172000-08-15 01:13:23 +00006from Tkinter import *
7import tkSimpleDialog
8import tkMessageBox
Kurt B. Kaiserfd182cd2001-07-14 03:58:25 +00009
10import webbrowser
David Scherer7aced172000-08-15 01:13:23 +000011import idlever
12import WindowList
Steven M. Gavac5976402002-01-04 03:06:08 +000013import SearchDialog
14import GrepDialog
15import ReplaceDialog
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +000016import PyParse
Steven M. Gavadc72f482002-01-03 11:51:07 +000017from configHandler import idleConf
Steven M. Gava3b55a892001-11-21 05:56:26 +000018import aboutDialog, textView, configDialog
David Scherer7aced172000-08-15 01:13:23 +000019
20# The default tab setting for a Text widget, in average-width characters.
21TK_TABWIDTH_DEFAULT = 8
22
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000023def _find_module(fullname, path=None):
24 """Version of imp.find_module() that handles hierarchical module names"""
25
26 file = None
27 for tgt in fullname.split('.'):
28 if file is not None:
29 file.close() # close intermediate files
30 (file, filename, descr) = imp.find_module(tgt, path)
31 if descr[2] == imp.PY_SOURCE:
32 break # find but not load the source file
33 module = imp.load_module(tgt, file, filename, descr)
Kurt B. Kaiser69e8afc2003-01-10 21:25:20 +000034 try:
35 path = module.__path__
36 except AttributeError:
37 raise ImportError, 'No source for module ' + module.__name__
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000038 return file, filename, descr
39
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +000040class EditorWindow(object):
David Scherer7aced172000-08-15 01:13:23 +000041 from Percolator import Percolator
42 from ColorDelegator import ColorDelegator
43 from UndoDelegator import UndoDelegator
44 from IOBinding import IOBinding
45 import Bindings
46 from Tkinter import Toplevel
47 from MultiStatusBar import MultiStatusBar
48
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000049 help_url = None
David Scherer7aced172000-08-15 01:13:23 +000050
51 def __init__(self, flist=None, filename=None, key=None, root=None):
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000052 if EditorWindow.help_url is None:
Thomas Heller84ef1532003-09-23 20:53:10 +000053 dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000054 if sys.platform.count('linux'):
55 # look for html docs in a couple of standard places
56 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
57 if os.path.isdir('/var/www/html/python/'): # "python2" rpm
58 dochome = '/var/www/html/python/index.html'
59 else:
60 basepath = '/usr/share/doc/' # standard location
61 dochome = os.path.join(basepath, pyver,
62 'Doc', 'index.html')
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +000063 elif sys.platform[:3] == 'win':
Kurt B. Kaiser090e6362004-07-21 03:33:58 +000064 chmfile = os.path.join(sys.prefix, 'Doc',
65 'Python%d%d.chm' % sys.version_info[:2])
Thomas Heller84ef1532003-09-23 20:53:10 +000066 if os.path.isfile(chmfile):
67 dochome = chmfile
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000068 dochome = os.path.normpath(dochome)
69 if os.path.isfile(dochome):
70 EditorWindow.help_url = dochome
71 else:
72 EditorWindow.help_url = "http://www.python.org/doc/current"
Steven M. Gavadc72f482002-01-03 11:51:07 +000073 currentTheme=idleConf.CurrentTheme()
David Scherer7aced172000-08-15 01:13:23 +000074 self.flist = flist
75 root = root or flist.root
76 self.root = root
David Scherer7aced172000-08-15 01:13:23 +000077 self.menubar = Menu(root)
Kurt B. Kaiser183403a2004-08-22 05:14:32 +000078 self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000079 if flist:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000080 self.tkinter_vars = flist.vars
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000081 #self.top.instance_dict makes flist.inversedict avalable to
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000082 #configDialog.py so it can access all EditorWindow instaces
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000083 self.top.instance_dict=flist.inversedict
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000084 else:
85 self.tkinter_vars = {} # keys: Tkinter event names
86 # values: Tkinter variable instances
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000087 self.recent_files_path=os.path.join(idleConf.GetUserCfgDir(),
Steven M. Gava1d46e402002-03-27 08:40:46 +000088 'recent-files.lst')
David Scherer7aced172000-08-15 01:13:23 +000089 self.vbar = vbar = Scrollbar(top, name='vbar')
90 self.text_frame = text_frame = Frame(top)
Kurt B. Kaiser1061e722003-01-04 01:43:53 +000091 self.width = idleConf.GetOption('main','EditorWindow','width')
Kurt B. Kaisera1dee062002-10-04 21:33:57 +000092 self.text = text = Text(text_frame, name='text', padx=5, wrap='none',
Steven M. Gavadc72f482002-01-03 11:51:07 +000093 foreground=idleConf.GetHighlight(currentTheme,
94 'normal',fgBg='fg'),
95 background=idleConf.GetHighlight(currentTheme,
96 'normal',fgBg='bg'),
97 highlightcolor=idleConf.GetHighlight(currentTheme,
98 'hilite',fgBg='fg'),
99 highlightbackground=idleConf.GetHighlight(currentTheme,
100 'hilite',fgBg='bg'),
101 insertbackground=idleConf.GetHighlight(currentTheme,
102 'cursor',fgBg='fg'),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000103 width=self.width,
Steven M. Gavadc72f482002-01-03 11:51:07 +0000104 height=idleConf.GetOption('main','EditorWindow','height') )
Kurt B. Kaiser183403a2004-08-22 05:14:32 +0000105 self.top.focused_widget = self.text
David Scherer7aced172000-08-15 01:13:23 +0000106
107 self.createmenubar()
108 self.apply_bindings()
109
110 self.top.protocol("WM_DELETE_WINDOW", self.close)
111 self.top.bind("<<close-window>>", self.close_event)
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000112 text.bind("<<cut>>", self.cut)
113 text.bind("<<copy>>", self.copy)
114 text.bind("<<paste>>", self.paste)
David Scherer7aced172000-08-15 01:13:23 +0000115 text.bind("<<center-insert>>", self.center_insert_event)
116 text.bind("<<help>>", self.help_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000117 text.bind("<<python-docs>>", self.python_docs)
118 text.bind("<<about-idle>>", self.about_dialog)
Steven M. Gava3b55a892001-11-21 05:56:26 +0000119 text.bind("<<open-config-dialog>>", self.config_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000120 text.bind("<<open-module>>", self.open_module)
121 text.bind("<<do-nothing>>", lambda event: "break")
122 text.bind("<<select-all>>", self.select_all)
123 text.bind("<<remove-selection>>", self.remove_selection)
Steven M. Gavac5976402002-01-04 03:06:08 +0000124 text.bind("<<find>>", self.find_event)
125 text.bind("<<find-again>>", self.find_again_event)
126 text.bind("<<find-in-files>>", self.find_in_files_event)
127 text.bind("<<find-selection>>", self.find_selection_event)
128 text.bind("<<replace>>", self.replace_event)
129 text.bind("<<goto-line>>", self.goto_line_event)
David Scherer7aced172000-08-15 01:13:23 +0000130 text.bind("<3>", self.right_menu_event)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000131 text.bind("<<smart-backspace>>",self.smart_backspace_event)
132 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
133 text.bind("<<smart-indent>>",self.smart_indent_event)
134 text.bind("<<indent-region>>",self.indent_region_event)
135 text.bind("<<dedent-region>>",self.dedent_region_event)
136 text.bind("<<comment-region>>",self.comment_region_event)
137 text.bind("<<uncomment-region>>",self.uncomment_region_event)
138 text.bind("<<tabify-region>>",self.tabify_region_event)
139 text.bind("<<untabify-region>>",self.untabify_region_event)
140 text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
141 text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000142 text.bind("<Left>", self.move_at_edge_if_selection(0))
143 text.bind("<Right>", self.move_at_edge_if_selection(1))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000144
David Scherer7aced172000-08-15 01:13:23 +0000145 if flist:
146 flist.inversedict[self] = key
147 if key:
148 flist.dict[key] = self
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000149 text.bind("<<open-new-window>>", self.new_callback)
David Scherer7aced172000-08-15 01:13:23 +0000150 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
151 text.bind("<<open-class-browser>>", self.open_class_browser)
152 text.bind("<<open-path-browser>>", self.open_path_browser)
153
Steven M. Gava898a3652001-10-07 11:10:44 +0000154 self.set_status_bar()
David Scherer7aced172000-08-15 01:13:23 +0000155 vbar['command'] = text.yview
156 vbar.pack(side=RIGHT, fill=Y)
David Scherer7aced172000-08-15 01:13:23 +0000157 text['yscrollcommand'] = vbar.set
Steven M. Gavab1585412002-03-12 00:21:56 +0000158 fontWeight='normal'
159 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
160 fontWeight='bold'
Steven M. Gavadc72f482002-01-03 11:51:07 +0000161 text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
Steven M. Gavab1585412002-03-12 00:21:56 +0000162 idleConf.GetOption('main','EditorWindow','font-size'),
163 fontWeight))
David Scherer7aced172000-08-15 01:13:23 +0000164 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
165 text.pack(side=TOP, fill=BOTH, expand=1)
166 text.focus_set()
167
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000168 # usetabs true -> literal tab characters are used by indent and
169 # dedent cmds, possibly mixed with spaces if
170 # indentwidth is not a multiple of tabwidth,
171 # which will cause Tabnanny to nag!
172 # false -> tab characters are converted to spaces by indent
173 # and dedent cmds, and ditto TAB keystrokes
174 self.usetabs = False
175
176 # indentwidth is the number of characters per logical indent level.
177 # Recommended Python default indent is four spaces.
178 self.indentwidth = 4
179
180 # tabwidth is the display width of a literal tab character.
181 # CAUTION: telling Tk to use anything other than its default
182 # tab setting causes it to use an entirely different tabbing algorithm,
183 # treating tab stops as fixed distances from the left margin.
184 # Nobody expects this, so for now tabwidth should never be changed.
185 self.tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed.
186 # indentwidth should be 8 when usetabs is True.
187
188 # If context_use_ps1 is true, parsing searches back for a ps1 line;
189 # else searches for a popular (if, def, ...) Python stmt.
190 self.context_use_ps1 = False
191
192 # When searching backwards for a reliable place to begin parsing,
193 # first start num_context_lines[0] lines back, then
194 # num_context_lines[1] lines back if that didn't work, and so on.
195 # The last value should be huge (larger than the # of lines in a
196 # conceivable file).
197 # Making the initial values larger slows things down more often.
198 self.num_context_lines = 50, 500, 5000000
199
David Scherer7aced172000-08-15 01:13:23 +0000200 self.per = per = self.Percolator(text)
201 if self.ispythonsource(filename):
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000202 self.color = color = self.ColorDelegator()
203 per.insertfilter(color)
David Scherer7aced172000-08-15 01:13:23 +0000204 else:
David Scherer7aced172000-08-15 01:13:23 +0000205 self.color = None
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000206
207 self.undo = undo = self.UndoDelegator()
208 per.insertfilter(undo)
209 text.undo_block_start = undo.undo_block_start
210 text.undo_block_stop = undo.undo_block_stop
211 undo.set_saved_change_hook(self.saved_change_hook)
212
213 # IOBinding implements file I/O and printing functionality
David Scherer7aced172000-08-15 01:13:23 +0000214 self.io = io = self.IOBinding(self)
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000215 io.set_filename_change_hook(self.filename_change_hook)
216
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000217 # Create the recent files submenu
218 self.recent_files_menu = Menu(self.menubar)
219 self.menudict['file'].insert_cascade(3, label='Recent Files',
220 underline=0,
221 menu=self.recent_files_menu)
222 self.update_recent_files_list()
David Scherer7aced172000-08-15 01:13:23 +0000223
David Scherer7aced172000-08-15 01:13:23 +0000224 if filename:
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000225 if os.path.exists(filename) and not os.path.isdir(filename):
David Scherer7aced172000-08-15 01:13:23 +0000226 io.loadfile(filename)
227 else:
228 io.set_filename(filename)
David Scherer7aced172000-08-15 01:13:23 +0000229 self.saved_change_hook()
230
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000231 self.set_indentation_params(self.ispythonsource(filename))
232
David Scherer7aced172000-08-15 01:13:23 +0000233 self.load_extensions()
234
235 menu = self.menudict.get('windows')
236 if menu:
237 end = menu.index("end")
238 if end is None:
239 end = -1
240 if end >= 0:
241 menu.add_separator()
242 end = end + 1
243 self.wmenu_end = end
244 WindowList.register_callback(self.postwindowsmenu)
245
246 # Some abstractions so IDLE extensions are cross-IDE
247 self.askyesno = tkMessageBox.askyesno
248 self.askinteger = tkSimpleDialog.askinteger
249 self.showerror = tkMessageBox.showerror
250
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000251 def new_callback(self, event):
252 dirname, basename = self.io.defaultfilename()
253 self.flist.new(dirname)
254 return "break"
255
David Scherer7aced172000-08-15 01:13:23 +0000256 def set_status_bar(self):
Steven M. Gava898a3652001-10-07 11:10:44 +0000257 self.status_bar = self.MultiStatusBar(self.top)
David Scherer7aced172000-08-15 01:13:23 +0000258 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
259 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
260 self.status_bar.pack(side=BOTTOM, fill=X)
261 self.text.bind('<KeyRelease>', self.set_line_and_column)
262 self.text.bind('<ButtonRelease>', self.set_line_and_column)
263 self.text.after_idle(self.set_line_and_column)
264
265 def set_line_and_column(self, event=None):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000266 line, column = self.text.index(INSERT).split('.')
David Scherer7aced172000-08-15 01:13:23 +0000267 self.status_bar.set_label('column', 'Col: %s' % column)
268 self.status_bar.set_label('line', 'Ln: %s' % line)
269
David Scherer7aced172000-08-15 01:13:23 +0000270 menu_specs = [
271 ("file", "_File"),
272 ("edit", "_Edit"),
273 ("format", "F_ormat"),
274 ("run", "_Run"),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000275 ("options", "_Options"),
David Scherer7aced172000-08-15 01:13:23 +0000276 ("windows", "_Windows"),
277 ("help", "_Help"),
278 ]
279
280 def createmenubar(self):
281 mbar = self.menubar
282 self.menudict = menudict = {}
283 for name, label in self.menu_specs:
284 underline, label = prepstr(label)
285 menudict[name] = menu = Menu(mbar, name=name)
286 mbar.add_cascade(label=label, menu=menu, underline=underline)
287 self.fill_menus()
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000288 self.base_helpmenu_length = self.menudict['help'].index(END)
289 self.reset_help_menu_entries()
David Scherer7aced172000-08-15 01:13:23 +0000290
291 def postwindowsmenu(self):
292 # Only called when Windows menu exists
David Scherer7aced172000-08-15 01:13:23 +0000293 menu = self.menudict['windows']
294 end = menu.index("end")
295 if end is None:
296 end = -1
297 if end > self.wmenu_end:
298 menu.delete(self.wmenu_end+1, end)
299 WindowList.add_windows_to_menu(menu)
300
301 rmenu = None
302
303 def right_menu_event(self, event):
304 self.text.tag_remove("sel", "1.0", "end")
305 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
306 if not self.rmenu:
307 self.make_rmenu()
308 rmenu = self.rmenu
309 self.event = event
310 iswin = sys.platform[:3] == 'win'
311 if iswin:
312 self.text.config(cursor="arrow")
313 rmenu.tk_popup(event.x_root, event.y_root)
314 if iswin:
315 self.text.config(cursor="ibeam")
316
317 rmenu_specs = [
318 # ("Label", "<<virtual-event>>"), ...
319 ("Close", "<<close-window>>"), # Example
320 ]
321
322 def make_rmenu(self):
323 rmenu = Menu(self.text, tearoff=0)
324 for label, eventname in self.rmenu_specs:
325 def command(text=self.text, eventname=eventname):
326 text.event_generate(eventname)
327 rmenu.add_command(label=label, command=command)
328 self.rmenu = rmenu
329
330 def about_dialog(self, event=None):
Kurt B. Kaiserd78b2302003-06-12 04:03:49 +0000331 aboutDialog.AboutDialog(self.top,'About IDLE')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000332
Steven M. Gava3b55a892001-11-21 05:56:26 +0000333 def config_dialog(self, event=None):
334 configDialog.ConfigDialog(self.top,'Settings')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000335
David Scherer7aced172000-08-15 01:13:23 +0000336 def help_dialog(self, event=None):
Steven M. Gavab9d07b52001-07-31 11:11:38 +0000337 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000338 textView.TextViewer(self.top,'Help',fn)
339
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000340 def python_docs(self, event=None):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000341 if sys.platform[:3] == 'win':
Steven M. Gava931625d2002-04-22 00:38:26 +0000342 os.startfile(self.help_url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000343 else:
344 webbrowser.open(self.help_url)
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000345 return "break"
David Scherer7aced172000-08-15 01:13:23 +0000346
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000347 def cut(self,event):
348 self.text.event_generate("<<Cut>>")
349 return "break"
350
351 def copy(self,event):
352 self.text.event_generate("<<Copy>>")
353 return "break"
354
355 def paste(self,event):
356 self.text.event_generate("<<Paste>>")
357 return "break"
358
David Scherer7aced172000-08-15 01:13:23 +0000359 def select_all(self, event=None):
360 self.text.tag_add("sel", "1.0", "end-1c")
361 self.text.mark_set("insert", "1.0")
362 self.text.see("insert")
363 return "break"
364
365 def remove_selection(self, event=None):
366 self.text.tag_remove("sel", "1.0", "end")
367 self.text.see("insert")
368
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000369 def move_at_edge_if_selection(self, edge_index):
370 """Cursor move begins at start or end of selection
371
372 When a left/right cursor key is pressed create and return to Tkinter a
373 function which causes a cursor move from the associated edge of the
374 selection.
375
376 """
377 self_text_index = self.text.index
378 self_text_mark_set = self.text.mark_set
379 edges_table = ("sel.first+1c", "sel.last-1c")
380 def move_at_edge(event):
381 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
382 try:
383 self_text_index("sel.first")
384 self_text_mark_set("insert", edges_table[edge_index])
385 except TclError:
386 pass
387 return move_at_edge
388
Steven M. Gavac5976402002-01-04 03:06:08 +0000389 def find_event(self, event):
390 SearchDialog.find(self.text)
391 return "break"
392
393 def find_again_event(self, event):
394 SearchDialog.find_again(self.text)
395 return "break"
396
397 def find_selection_event(self, event):
398 SearchDialog.find_selection(self.text)
399 return "break"
400
401 def find_in_files_event(self, event):
402 GrepDialog.grep(self.text, self.io, self.flist)
403 return "break"
404
405 def replace_event(self, event):
406 ReplaceDialog.replace(self.text)
407 return "break"
408
409 def goto_line_event(self, event):
410 text = self.text
411 lineno = tkSimpleDialog.askinteger("Goto",
412 "Go to line number:",parent=text)
413 if lineno is None:
414 return "break"
415 if lineno <= 0:
416 text.bell()
417 return "break"
418 text.mark_set("insert", "%d.0" % lineno)
419 text.see("insert")
420
David Scherer7aced172000-08-15 01:13:23 +0000421 def open_module(self, event=None):
422 # XXX Shouldn't this be in IOBinding or in FileList?
423 try:
424 name = self.text.get("sel.first", "sel.last")
425 except TclError:
426 name = ""
427 else:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000428 name = name.strip()
Guido van Rossum852f35b2003-06-05 11:36:55 +0000429 name = tkSimpleDialog.askstring("Module",
430 "Enter the name of a Python module\n"
431 "to search on sys.path and open:",
432 parent=self.text, initialvalue=name)
433 if name:
434 name = name.strip()
David Scherer7aced172000-08-15 01:13:23 +0000435 if not name:
Guido van Rossum852f35b2003-06-05 11:36:55 +0000436 return
David Scherer7aced172000-08-15 01:13:23 +0000437 # XXX Ought to insert current file's directory in front of path
438 try:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000439 (f, file, (suffix, mode, type)) = _find_module(name)
David Scherer7aced172000-08-15 01:13:23 +0000440 except (NameError, ImportError), msg:
441 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
442 return
443 if type != imp.PY_SOURCE:
444 tkMessageBox.showerror("Unsupported type",
445 "%s is not a source module" % name, parent=self.text)
446 return
447 if f:
448 f.close()
449 if self.flist:
450 self.flist.open(file)
451 else:
452 self.io.loadfile(file)
453
454 def open_class_browser(self, event=None):
455 filename = self.io.filename
456 if not filename:
457 tkMessageBox.showerror(
458 "No filename",
459 "This buffer has no associated filename",
460 master=self.text)
461 self.text.focus_set()
462 return None
463 head, tail = os.path.split(filename)
464 base, ext = os.path.splitext(tail)
465 import ClassBrowser
466 ClassBrowser.ClassBrowser(self.flist, base, [head])
467
468 def open_path_browser(self, event=None):
469 import PathBrowser
470 PathBrowser.PathBrowser(self.flist)
471
472 def gotoline(self, lineno):
473 if lineno is not None and lineno > 0:
474 self.text.mark_set("insert", "%d.0" % lineno)
475 self.text.tag_remove("sel", "1.0", "end")
476 self.text.tag_add("sel", "insert", "insert +1l")
477 self.center()
478
479 def ispythonsource(self, filename):
480 if not filename:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000481 return True
David Scherer7aced172000-08-15 01:13:23 +0000482 base, ext = os.path.splitext(os.path.basename(filename))
483 if os.path.normcase(ext) in (".py", ".pyw"):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000484 return True
David Scherer7aced172000-08-15 01:13:23 +0000485 try:
486 f = open(filename)
487 line = f.readline()
488 f.close()
489 except IOError:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000490 return False
Kurt B. Kaiser83a35602002-12-20 17:18:03 +0000491 return line.startswith('#!') and line.find('python') >= 0
David Scherer7aced172000-08-15 01:13:23 +0000492
493 def close_hook(self):
494 if self.flist:
495 self.flist.close_edit(self)
496
497 def set_close_hook(self, close_hook):
498 self.close_hook = close_hook
499
500 def filename_change_hook(self):
501 if self.flist:
502 self.flist.filename_changed_edit(self)
503 self.saved_change_hook()
Kurt B. Kaiser260cb902003-06-06 21:58:38 +0000504 self.top.update_windowlist_registry(self)
David Scherer7aced172000-08-15 01:13:23 +0000505 if self.ispythonsource(self.io.filename):
506 self.addcolorizer()
507 else:
508 self.rmcolorizer()
509
510 def addcolorizer(self):
511 if self.color:
512 return
David Scherer7aced172000-08-15 01:13:23 +0000513 self.per.removefilter(self.undo)
514 self.color = self.ColorDelegator()
515 self.per.insertfilter(self.color)
516 self.per.insertfilter(self.undo)
517
518 def rmcolorizer(self):
519 if not self.color:
520 return
David Scherer7aced172000-08-15 01:13:23 +0000521 self.per.removefilter(self.undo)
522 self.per.removefilter(self.color)
523 self.color = None
524 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000525
Steven M. Gavab77d3432002-03-02 07:16:21 +0000526 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000527 "Update the colour theme if it is changed"
528 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000529 if self.color:
530 self.color = self.ColorDelegator()
531 self.per.insertfilter(self.color)
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000532 theme = idleConf.GetOption('main','Theme','name')
533 self.text.config(idleConf.GetHighlight(theme, "normal"))
David Scherer7aced172000-08-15 01:13:23 +0000534
Steven M. Gavab1585412002-03-12 00:21:56 +0000535 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000536 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000537 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000538 fontWeight='normal'
539 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
540 fontWeight='bold'
541 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
542 idleConf.GetOption('main','EditorWindow','font-size'),
543 fontWeight))
544
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000545 def ResetKeybindings(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000546 "Update the keybindings if they are changed"
547 # Called from configDialog.py
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000548 self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
549 keydefs = self.Bindings.default_keydefs
550 for event, keylist in keydefs.items():
551 self.text.event_delete(event)
552 self.apply_bindings()
553 #update menu accelerators
554 menuEventDict={}
555 for menu in self.Bindings.menudefs:
556 menuEventDict[menu[0]]={}
557 for item in menu[1]:
558 if item:
559 menuEventDict[menu[0]][prepstr(item[0])[1]]=item[1]
560 for menubarItem in self.menudict.keys():
561 menu=self.menudict[menubarItem]
562 end=menu.index(END)+1
563 for index in range(0,end):
564 if menu.type(index)=='command':
565 accel=menu.entrycget(index,'accelerator')
566 if accel:
567 itemName=menu.entrycget(index,'label')
568 event=''
569 if menuEventDict.has_key(menubarItem):
570 if menuEventDict[menubarItem].has_key(itemName):
571 event=menuEventDict[menubarItem][itemName]
572 if event:
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000573 accel=get_accelerator(keydefs, event)
574 menu.entryconfig(index,accelerator=accel)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000575
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000576 def reset_help_menu_entries(self):
577 "Update the additional help entries on the Help menu"
578 help_list = idleConf.GetAllExtraHelpSourcesList()
579 helpmenu = self.menudict['help']
580 # first delete the extra help entries, if any
581 helpmenu_length = helpmenu.index(END)
582 if helpmenu_length > self.base_helpmenu_length:
583 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
584 # then rebuild them
585 if help_list:
586 helpmenu.add_separator()
587 for entry in help_list:
588 cmd = self.__extra_help_callback(entry[1])
589 helpmenu.add_command(label=entry[0], command=cmd)
590 # and update the menu dictionary
591 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000592
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000593 def __extra_help_callback(self, helpfile):
594 "Create a callback with the helpfile value frozen at definition time"
595 def display_extra_help(helpfile=helpfile):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000596 if not (helpfile.startswith('www') or helpfile.startswith('http')):
597 url = os.path.normpath(helpfile)
598 if sys.platform[:3] == 'win':
599 os.startfile(helpfile)
600 else:
601 webbrowser.open(helpfile)
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000602 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000603
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000604 def update_recent_files_list(self, new_file=None):
605 "Load and update the recent files list and menus"
606 rf_list = []
607 if os.path.exists(self.recent_files_path):
608 rf_list_file = open(self.recent_files_path,'r')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000609 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000610 rf_list = rf_list_file.readlines()
Steven M. Gava1d46e402002-03-27 08:40:46 +0000611 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000612 rf_list_file.close()
613 if new_file:
614 new_file = os.path.abspath(new_file) + '\n'
615 if new_file in rf_list:
616 rf_list.remove(new_file) # move to top
617 rf_list.insert(0, new_file)
618 # clean and save the recent files list
619 bad_paths = []
620 for path in rf_list:
621 if '\0' in path or not os.path.exists(path[0:-1]):
622 bad_paths.append(path)
623 rf_list = [path for path in rf_list if path not in bad_paths]
624 ulchars = "1234567890ABCDEFGHIJK"
625 rf_list = rf_list[0:len(ulchars)]
626 rf_file = open(self.recent_files_path, 'w')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000627 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000628 rf_file.writelines(rf_list)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000629 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000630 rf_file.close()
631 # for each edit window instance, construct the recent files menu
632 for instance in self.top.instance_dict.keys():
633 menu = instance.recent_files_menu
634 menu.delete(1, END) # clear, and rebuild:
635 for i, file in zip(count(), rf_list):
636 file_name = file[0:-1] # zap \n
637 callback = instance.__recent_file_callback(file_name)
638 menu.add_command(label=ulchars[i] + " " + file_name,
639 command=callback,
640 underline=0)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000641
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000642 def __recent_file_callback(self, file_name):
643 def open_recent_file(fn_closure=file_name):
644 self.io.open(editFile=fn_closure)
645 return open_recent_file
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000646
David Scherer7aced172000-08-15 01:13:23 +0000647 def saved_change_hook(self):
648 short = self.short_title()
649 long = self.long_title()
650 if short and long:
651 title = short + " - " + long
652 elif short:
653 title = short
654 elif long:
655 title = long
656 else:
657 title = "Untitled"
658 icon = short or long or title
659 if not self.get_saved():
660 title = "*%s*" % title
661 icon = "*%s" % icon
662 self.top.wm_title(title)
663 self.top.wm_iconname(icon)
664
665 def get_saved(self):
666 return self.undo.get_saved()
667
668 def set_saved(self, flag):
669 self.undo.set_saved(flag)
670
671 def reset_undo(self):
672 self.undo.reset_undo()
673
674 def short_title(self):
675 filename = self.io.filename
676 if filename:
677 filename = os.path.basename(filename)
678 return filename
679
680 def long_title(self):
681 return self.io.filename or ""
682
683 def center_insert_event(self, event):
684 self.center()
685
686 def center(self, mark="insert"):
687 text = self.text
688 top, bot = self.getwindowlines()
689 lineno = self.getlineno(mark)
690 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000691 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000692 text.yview(float(newtop))
693
694 def getwindowlines(self):
695 text = self.text
696 top = self.getlineno("@0,0")
697 bot = self.getlineno("@0,65535")
698 if top == bot and text.winfo_height() == 1:
699 # Geometry manager hasn't run yet
700 height = int(text['height'])
701 bot = top + height - 1
702 return top, bot
703
704 def getlineno(self, mark="insert"):
705 text = self.text
706 return int(float(text.index(mark)))
707
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000708 def get_geometry(self):
709 "Return (width, height, x, y)"
710 geom = self.top.wm_geometry()
711 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
712 tuple = (map(int, m.groups()))
713 return tuple
714
David Scherer7aced172000-08-15 01:13:23 +0000715 def close_event(self, event):
716 self.close()
717
718 def maybesave(self):
719 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000720 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000721 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000722 self.top.deiconify()
723 self.top.lower()
724 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000725 return self.io.maybesave()
726
727 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000728 reply = self.maybesave()
729 if reply != "cancel":
730 self._close()
731 return reply
732
733 def _close(self):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000734 if self.io.filename:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000735 self.update_recent_files_list(new_file=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000736 WindowList.unregister_callback(self.postwindowsmenu)
737 if self.close_hook:
738 self.close_hook()
739 self.flist = None
740 colorizing = 0
741 self.unload_extensions()
742 self.io.close(); self.io = None
743 self.undo = None # XXX
744 if self.color:
745 colorizing = self.color.colorizing
746 doh = colorizing and self.top
747 self.color.close(doh) # Cancel colorization
748 self.text = None
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000749 self.tkinter_vars = None
David Scherer7aced172000-08-15 01:13:23 +0000750 self.per.close(); self.per = None
751 if not colorizing:
752 self.top.destroy()
753
754 def load_extensions(self):
755 self.extensions = {}
756 self.load_standard_extensions()
757
758 def unload_extensions(self):
759 for ins in self.extensions.values():
760 if hasattr(ins, "close"):
761 ins.close()
762 self.extensions = {}
763
764 def load_standard_extensions(self):
765 for name in self.get_standard_extension_names():
766 try:
767 self.load_extension(name)
768 except:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000769 print "Failed to load extension", repr(name)
David Scherer7aced172000-08-15 01:13:23 +0000770 import traceback
771 traceback.print_exc()
772
773 def get_standard_extension_names(self):
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000774 return idleConf.GetExtensions(editor_only=True)
David Scherer7aced172000-08-15 01:13:23 +0000775
776 def load_extension(self, name):
Kurt B. Kaiserb00e89f2005-01-18 00:54:58 +0000777 try:
778 mod = __import__(name, globals(), locals(), [])
779 except ImportError:
780 print "\nFailed to import extension: ", name
781 return
David Scherer7aced172000-08-15 01:13:23 +0000782 cls = getattr(mod, name)
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000783 keydefs = idleConf.GetExtensionBindings(name)
784 if hasattr(cls, "menudefs"):
785 self.fill_menus(cls.menudefs, keydefs)
David Scherer7aced172000-08-15 01:13:23 +0000786 ins = cls(self)
787 self.extensions[name] = ins
David Scherer7aced172000-08-15 01:13:23 +0000788 if keydefs:
789 self.apply_bindings(keydefs)
790 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000791 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000792 while methodname[:1] == '<':
793 methodname = methodname[1:]
794 while methodname[-1:] == '>':
795 methodname = methodname[:-1]
796 methodname = methodname + "_event"
797 if hasattr(ins, methodname):
798 self.text.bind(vevent, getattr(ins, methodname))
David Scherer7aced172000-08-15 01:13:23 +0000799
800 def apply_bindings(self, keydefs=None):
801 if keydefs is None:
802 keydefs = self.Bindings.default_keydefs
803 text = self.text
804 text.keydefs = keydefs
805 for event, keylist in keydefs.items():
806 if keylist:
Raymond Hettinger931237e2003-07-09 18:48:24 +0000807 text.event_add(event, *keylist)
David Scherer7aced172000-08-15 01:13:23 +0000808
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000809 def fill_menus(self, menudefs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000810 """Add appropriate entries to the menus and submenus
811
812 Menus that are absent or None in self.menudict are ignored.
813 """
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000814 if menudefs is None:
815 menudefs = self.Bindings.menudefs
David Scherer7aced172000-08-15 01:13:23 +0000816 if keydefs is None:
817 keydefs = self.Bindings.default_keydefs
818 menudict = self.menudict
819 text = self.text
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000820 for mname, entrylist in menudefs:
David Scherer7aced172000-08-15 01:13:23 +0000821 menu = menudict.get(mname)
822 if not menu:
823 continue
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000824 for entry in entrylist:
825 if not entry:
David Scherer7aced172000-08-15 01:13:23 +0000826 menu.add_separator()
827 else:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000828 label, eventname = entry
David Scherer7aced172000-08-15 01:13:23 +0000829 checkbutton = (label[:1] == '!')
830 if checkbutton:
831 label = label[1:]
832 underline, label = prepstr(label)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000833 accelerator = get_accelerator(keydefs, eventname)
834 def command(text=text, eventname=eventname):
835 text.event_generate(eventname)
David Scherer7aced172000-08-15 01:13:23 +0000836 if checkbutton:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000837 var = self.get_var_obj(eventname, BooleanVar)
David Scherer7aced172000-08-15 01:13:23 +0000838 menu.add_checkbutton(label=label, underline=underline,
839 command=command, accelerator=accelerator,
840 variable=var)
841 else:
842 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000843 command=command,
844 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000845
846 def getvar(self, name):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000847 var = self.get_var_obj(name)
David Scherer7aced172000-08-15 01:13:23 +0000848 if var:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000849 value = var.get()
850 return value
851 else:
852 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000853
854 def setvar(self, name, value, vartype=None):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000855 var = self.get_var_obj(name, vartype)
David Scherer7aced172000-08-15 01:13:23 +0000856 if var:
857 var.set(value)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000858 else:
859 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000860
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000861 def get_var_obj(self, name, vartype=None):
862 var = self.tkinter_vars.get(name)
David Scherer7aced172000-08-15 01:13:23 +0000863 if not var and vartype:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000864 # create a Tkinter variable object with self.text as master:
865 self.tkinter_vars[name] = var = vartype(self.text)
David Scherer7aced172000-08-15 01:13:23 +0000866 return var
867
868 # Tk implementations of "virtual text methods" -- each platform
869 # reusing IDLE's support code needs to define these for its GUI's
870 # flavor of widget.
871
872 # Is character at text_index in a Python string? Return 0 for
873 # "guaranteed no", true for anything else. This info is expensive
874 # to compute ab initio, but is probably already known by the
875 # platform's colorizer.
876
877 def is_char_in_string(self, text_index):
878 if self.color:
879 # Return true iff colorizer hasn't (re)gotten this far
880 # yet, or the character is tagged as being in a string
881 return self.text.tag_prevrange("TODO", text_index) or \
882 "STRING" in self.text.tag_names(text_index)
883 else:
884 # The colorizer is missing: assume the worst
885 return 1
886
887 # If a selection is defined in the text widget, return (start,
888 # end) as Tkinter text indices, otherwise return (None, None)
889 def get_selection_indices(self):
890 try:
891 first = self.text.index("sel.first")
892 last = self.text.index("sel.last")
893 return first, last
894 except TclError:
895 return None, None
896
897 # Return the text widget's current view of what a tab stop means
898 # (equivalent width in spaces).
899
900 def get_tabwidth(self):
901 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
902 return int(current)
903
904 # Set the text widget's current view of what a tab stop means.
905
906 def set_tabwidth(self, newtabwidth):
907 text = self.text
908 if self.get_tabwidth() != newtabwidth:
909 pixels = text.tk.call("font", "measure", text["font"],
910 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000911 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000912 text.configure(tabs=pixels)
913
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000914 # If ispythonsource and guess are true, guess a good value for
915 # indentwidth based on file content (if possible), and if
916 # indentwidth != tabwidth set usetabs false.
917 # In any case, adjust the Text widget's view of what a tab
918 # character means.
919
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000920 def set_indentation_params(self, ispythonsource, guess=True):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000921 if guess and ispythonsource:
922 i = self.guess_indent()
923 if 2 <= i <= 8:
924 self.indentwidth = i
925 if self.indentwidth != self.tabwidth:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000926 self.usetabs = False
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000927 self.set_tabwidth(self.tabwidth)
928
929 def smart_backspace_event(self, event):
930 text = self.text
931 first, last = self.get_selection_indices()
932 if first and last:
933 text.delete(first, last)
934 text.mark_set("insert", first)
935 return "break"
936 # Delete whitespace left, until hitting a real char or closest
937 # preceding virtual tab stop.
938 chars = text.get("insert linestart", "insert")
939 if chars == '':
940 if text.compare("insert", ">", "1.0"):
941 # easy: delete preceding newline
942 text.delete("insert-1c")
943 else:
944 text.bell() # at start of buffer
945 return "break"
946 if chars[-1] not in " \t":
947 # easy: delete preceding real char
948 text.delete("insert-1c")
949 return "break"
950 # Ick. It may require *inserting* spaces if we back up over a
951 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000952 tabwidth = self.tabwidth
953 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000954 assert have > 0
955 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000956 # Debug prompt is multilined....
957 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000958 ncharsdeleted = 0
959 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000960 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +0000961 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000962 chars = chars[:-1]
963 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000964 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000965 if have <= want or chars[-1] not in " \t":
966 break
967 text.undo_block_start()
968 text.delete("insert-%dc" % ncharsdeleted, "insert")
969 if have < want:
970 text.insert("insert", ' ' * (want - have))
971 text.undo_block_stop()
972 return "break"
973
974 def smart_indent_event(self, event):
975 # if intraline selection:
976 # delete it
977 # elif multiline selection:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000978 # do indent-region
979 # else:
980 # indent one level
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000981 text = self.text
982 first, last = self.get_selection_indices()
983 text.undo_block_start()
984 try:
985 if first and last:
986 if index2line(first) != index2line(last):
987 return self.indent_region_event(event)
988 text.delete(first, last)
989 text.mark_set("insert", first)
990 prefix = text.get("insert linestart", "insert")
991 raw, effective = classifyws(prefix, self.tabwidth)
992 if raw == len(prefix):
993 # only whitespace to the left
994 self.reindent_to(effective + self.indentwidth)
995 else:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000996 # tab to the next 'stop' within or to right of line's text:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000997 if self.usetabs:
998 pad = '\t'
999 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001000 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001001 n = self.indentwidth
1002 pad = ' ' * (n - effective % n)
1003 text.insert("insert", pad)
1004 text.see("insert")
1005 return "break"
1006 finally:
1007 text.undo_block_stop()
1008
1009 def newline_and_indent_event(self, event):
1010 text = self.text
1011 first, last = self.get_selection_indices()
1012 text.undo_block_start()
1013 try:
1014 if first and last:
1015 text.delete(first, last)
1016 text.mark_set("insert", first)
1017 line = text.get("insert linestart", "insert")
1018 i, n = 0, len(line)
1019 while i < n and line[i] in " \t":
1020 i = i+1
1021 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001022 # the cursor is in or at leading indentation in a continuation
1023 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001024 text.insert("insert linestart", '\n')
1025 return "break"
1026 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001027 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001028 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001029 last_line_of_prompt = sys.ps1.split('\n')[-1]
1030 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001031 line = line[:-1]
1032 i = i+1
1033 if i:
1034 text.delete("insert - %d chars" % i, "insert")
1035 # strip whitespace after insert point
1036 while text.get("insert") in " \t":
1037 text.delete("insert")
1038 # start new line
1039 text.insert("insert", '\n')
1040
1041 # adjust indentation for continuations and block
1042 # open/close first need to find the last stmt
1043 lno = index2line(text.index('insert'))
1044 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1045 for context in self.num_context_lines:
1046 startat = max(lno - context, 1)
Walter Dörwald70a6b492004-02-12 17:35:32 +00001047 startatindex = repr(startat) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001048 rawtext = text.get(startatindex, "insert")
1049 y.set_str(rawtext)
1050 bod = y.find_good_parse_start(
1051 self.context_use_ps1,
1052 self._build_char_in_string_func(startatindex))
1053 if bod is not None or startat == 1:
1054 break
1055 y.set_lo(bod or 0)
1056 c = y.get_continuation_type()
1057 if c != PyParse.C_NONE:
1058 # The current stmt hasn't ended yet.
1059 if c == PyParse.C_STRING:
1060 # inside a string; just mimic the current indent
1061 text.insert("insert", indent)
1062 elif c == PyParse.C_BRACKET:
1063 # line up with the first (if any) element of the
1064 # last open bracket structure; else indent one
1065 # level beyond the indent of the line with the
1066 # last open bracket
1067 self.reindent_to(y.compute_bracket_indent())
1068 elif c == PyParse.C_BACKSLASH:
1069 # if more than one line in this stmt already, just
1070 # mimic the current indent; else if initial line
1071 # has a start on an assignment stmt, indent to
1072 # beyond leftmost =; else to beyond first chunk of
1073 # non-whitespace on initial line
1074 if y.get_num_lines_in_stmt() > 1:
1075 text.insert("insert", indent)
1076 else:
1077 self.reindent_to(y.compute_backslash_indent())
1078 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001079 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001080 return "break"
1081
1082 # This line starts a brand new stmt; indent relative to
1083 # indentation of initial line of closest preceding
1084 # interesting stmt.
1085 indent = y.get_base_indent_string()
1086 text.insert("insert", indent)
1087 if y.is_block_opener():
1088 self.smart_indent_event(event)
1089 elif indent and y.is_block_closer():
1090 self.smart_backspace_event(event)
1091 return "break"
1092 finally:
1093 text.see("insert")
1094 text.undo_block_stop()
1095
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001096 # Our editwin provides a is_char_in_string function that works
1097 # with a Tk text index, but PyParse only knows about offsets into
1098 # a string. This builds a function for PyParse that accepts an
1099 # offset.
1100
1101 def _build_char_in_string_func(self, startindex):
1102 def inner(offset, _startindex=startindex,
1103 _icis=self.is_char_in_string):
1104 return _icis(_startindex + "+%dc" % offset)
1105 return inner
1106
1107 def indent_region_event(self, event):
1108 head, tail, chars, lines = self.get_region()
1109 for pos in range(len(lines)):
1110 line = lines[pos]
1111 if line:
1112 raw, effective = classifyws(line, self.tabwidth)
1113 effective = effective + self.indentwidth
1114 lines[pos] = self._make_blanks(effective) + line[raw:]
1115 self.set_region(head, tail, chars, lines)
1116 return "break"
1117
1118 def dedent_region_event(self, event):
1119 head, tail, chars, lines = self.get_region()
1120 for pos in range(len(lines)):
1121 line = lines[pos]
1122 if line:
1123 raw, effective = classifyws(line, self.tabwidth)
1124 effective = max(effective - self.indentwidth, 0)
1125 lines[pos] = self._make_blanks(effective) + line[raw:]
1126 self.set_region(head, tail, chars, lines)
1127 return "break"
1128
1129 def comment_region_event(self, event):
1130 head, tail, chars, lines = self.get_region()
1131 for pos in range(len(lines) - 1):
1132 line = lines[pos]
1133 lines[pos] = '##' + line
1134 self.set_region(head, tail, chars, lines)
1135
1136 def uncomment_region_event(self, event):
1137 head, tail, chars, lines = self.get_region()
1138 for pos in range(len(lines)):
1139 line = lines[pos]
1140 if not line:
1141 continue
1142 if line[:2] == '##':
1143 line = line[2:]
1144 elif line[:1] == '#':
1145 line = line[1:]
1146 lines[pos] = line
1147 self.set_region(head, tail, chars, lines)
1148
1149 def tabify_region_event(self, event):
1150 head, tail, chars, lines = self.get_region()
1151 tabwidth = self._asktabwidth()
1152 for pos in range(len(lines)):
1153 line = lines[pos]
1154 if line:
1155 raw, effective = classifyws(line, tabwidth)
1156 ntabs, nspaces = divmod(effective, tabwidth)
1157 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1158 self.set_region(head, tail, chars, lines)
1159
1160 def untabify_region_event(self, event):
1161 head, tail, chars, lines = self.get_region()
1162 tabwidth = self._asktabwidth()
1163 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001164 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001165 self.set_region(head, tail, chars, lines)
1166
1167 def toggle_tabs_event(self, event):
1168 if self.askyesno(
1169 "Toggle tabs",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001170 "Turn tabs " + ("on", "off")[self.usetabs] +
1171 "?\nIndent width " +
1172 ("will be", "remains at")[self.usetabs] + " 8.",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001173 parent=self.text):
1174 self.usetabs = not self.usetabs
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001175 # Try to prevent mixed tabs/spaces.
1176 # User must reset indent width manually after using tabs
1177 # if he insists on getting into trouble.
1178 self.indentwidth = 8
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001179 return "break"
1180
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001181 # XXX this isn't bound to anything -- see tabwidth comments
1182## def change_tabwidth_event(self, event):
1183## new = self._asktabwidth()
1184## if new != self.tabwidth:
1185## self.tabwidth = new
1186## self.set_indentation_params(0, guess=0)
1187## return "break"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001188
1189 def change_indentwidth_event(self, event):
1190 new = self.askinteger(
1191 "Indent width",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001192 "New indent width (2-16)\n(Always use 8 when using tabs)",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001193 parent=self.text,
1194 initialvalue=self.indentwidth,
1195 minvalue=2,
1196 maxvalue=16)
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001197 if new and new != self.indentwidth and not self.usetabs:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001198 self.indentwidth = new
1199 return "break"
1200
1201 def get_region(self):
1202 text = self.text
1203 first, last = self.get_selection_indices()
1204 if first and last:
1205 head = text.index(first + " linestart")
1206 tail = text.index(last + "-1c lineend +1c")
1207 else:
1208 head = text.index("insert linestart")
1209 tail = text.index("insert lineend +1c")
1210 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001211 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001212 return head, tail, chars, lines
1213
1214 def set_region(self, head, tail, chars, lines):
1215 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001216 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001217 if newchars == chars:
1218 text.bell()
1219 return
1220 text.tag_remove("sel", "1.0", "end")
1221 text.mark_set("insert", head)
1222 text.undo_block_start()
1223 text.delete(head, tail)
1224 text.insert(head, newchars)
1225 text.undo_block_stop()
1226 text.tag_add("sel", head, "insert")
1227
1228 # Make string that displays as n leading blanks.
1229
1230 def _make_blanks(self, n):
1231 if self.usetabs:
1232 ntabs, nspaces = divmod(n, self.tabwidth)
1233 return '\t' * ntabs + ' ' * nspaces
1234 else:
1235 return ' ' * n
1236
1237 # Delete from beginning of line to insert point, then reinsert
1238 # column logical (meaning use tabs if appropriate) spaces.
1239
1240 def reindent_to(self, column):
1241 text = self.text
1242 text.undo_block_start()
1243 if text.compare("insert linestart", "!=", "insert"):
1244 text.delete("insert linestart", "insert")
1245 if column:
1246 text.insert("insert", self._make_blanks(column))
1247 text.undo_block_stop()
1248
1249 def _asktabwidth(self):
1250 return self.askinteger(
1251 "Tab width",
1252 "Spaces per tab? (2-16)",
1253 parent=self.text,
1254 initialvalue=self.indentwidth,
1255 minvalue=2,
1256 maxvalue=16) or self.tabwidth
1257
1258 # Guess indentwidth from text content.
1259 # Return guessed indentwidth. This should not be believed unless
1260 # it's in a reasonable range (e.g., it will be 0 if no indented
1261 # blocks are found).
1262
1263 def guess_indent(self):
1264 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1265 if opener and indented:
1266 raw, indentsmall = classifyws(opener, self.tabwidth)
1267 raw, indentlarge = classifyws(indented, self.tabwidth)
1268 else:
1269 indentsmall = indentlarge = 0
1270 return indentlarge - indentsmall
1271
1272# "line.col" -> line, as an int
1273def index2line(index):
1274 return int(float(index))
1275
1276# Look at the leading whitespace in s.
1277# Return pair (# of leading ws characters,
1278# effective # of leading blanks after expanding
1279# tabs to width tabwidth)
1280
1281def classifyws(s, tabwidth):
1282 raw = effective = 0
1283 for ch in s:
1284 if ch == ' ':
1285 raw = raw + 1
1286 effective = effective + 1
1287 elif ch == '\t':
1288 raw = raw + 1
1289 effective = (effective // tabwidth + 1) * tabwidth
1290 else:
1291 break
1292 return raw, effective
1293
1294import tokenize
1295_tokenize = tokenize
1296del tokenize
1297
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +00001298class IndentSearcher(object):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001299
1300 # .run() chews over the Text widget, looking for a block opener
1301 # and the stmt following it. Returns a pair,
1302 # (line containing block opener, line containing stmt)
1303 # Either or both may be None.
1304
1305 def __init__(self, text, tabwidth):
1306 self.text = text
1307 self.tabwidth = tabwidth
1308 self.i = self.finished = 0
1309 self.blkopenline = self.indentedline = None
1310
1311 def readline(self):
1312 if self.finished:
1313 return ""
1314 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001315 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001316 if self.text.compare(mark, ">=", "end"):
1317 return ""
1318 return self.text.get(mark, mark + " lineend+1c")
1319
1320 def tokeneater(self, type, token, start, end, line,
1321 INDENT=_tokenize.INDENT,
1322 NAME=_tokenize.NAME,
1323 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1324 if self.finished:
1325 pass
1326 elif type == NAME and token in OPENERS:
1327 self.blkopenline = line
1328 elif type == INDENT and self.blkopenline:
1329 self.indentedline = line
1330 self.finished = 1
1331
1332 def run(self):
1333 save_tabsize = _tokenize.tabsize
1334 _tokenize.tabsize = self.tabwidth
1335 try:
1336 try:
1337 _tokenize.tokenize(self.readline, self.tokeneater)
1338 except _tokenize.TokenError:
1339 # since we cut off the tokenizer early, we can trigger
1340 # spurious errors
1341 pass
1342 finally:
1343 _tokenize.tabsize = save_tabsize
1344 return self.blkopenline, self.indentedline
1345
1346### end autoindent code ###
1347
David Scherer7aced172000-08-15 01:13:23 +00001348def prepstr(s):
1349 # Helper to extract the underscore from a string, e.g.
1350 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001351 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001352 if i >= 0:
1353 s = s[:i] + s[i+1:]
1354 return i, s
1355
1356
1357keynames = {
1358 'bracketleft': '[',
1359 'bracketright': ']',
1360 'slash': '/',
1361}
1362
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001363def get_accelerator(keydefs, eventname):
1364 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001365 if not keylist:
1366 return ""
1367 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001368 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001369 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1370 s = re.sub("Key-", "", s)
1371 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1372 s = re.sub("Control-", "Ctrl-", s)
1373 s = re.sub("-", "+", s)
1374 s = re.sub("><", " ", s)
1375 s = re.sub("<", "", s)
1376 s = re.sub(">", "", s)
1377 return s
1378
1379
1380def fixwordbreaks(root):
1381 # Make sure that Tk's double-click and next/previous word
1382 # operations use our definition of a word (i.e. an identifier)
1383 tk = root.tk
1384 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1385 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1386 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1387
1388
1389def test():
1390 root = Tk()
1391 fixwordbreaks(root)
1392 root.withdraw()
1393 if sys.argv[1:]:
1394 filename = sys.argv[1]
1395 else:
1396 filename = None
1397 edit = EditorWindow(root=root, filename=filename)
1398 edit.set_close_hook(root.quit)
1399 root.mainloop()
1400 root.destroy()
1401
1402if __name__ == '__main__':
1403 test()