blob: 41d26e56d192661d09f49f00147d2226099a0dbc [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
David Scherer7aced172000-08-15 01:13:23 +000040class EditorWindow:
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':
Thomas Heller84ef1532003-09-23 20:53:10 +000064 chmfile = os.path.join(sys.prefix, "Python%d%d.chm" % sys.version_info[:2])
65 if os.path.isfile(chmfile):
66 dochome = chmfile
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000067 dochome = os.path.normpath(dochome)
68 if os.path.isfile(dochome):
69 EditorWindow.help_url = dochome
70 else:
71 EditorWindow.help_url = "http://www.python.org/doc/current"
Steven M. Gavadc72f482002-01-03 11:51:07 +000072 currentTheme=idleConf.CurrentTheme()
David Scherer7aced172000-08-15 01:13:23 +000073 self.flist = flist
74 root = root or flist.root
75 self.root = root
David Scherer7aced172000-08-15 01:13:23 +000076 self.menubar = Menu(root)
77 self.top = top = self.Toplevel(root, menu=self.menubar)
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000078 if flist:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000079 self.tkinter_vars = flist.vars
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000080 #self.top.instance_dict makes flist.inversedict avalable to
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000081 #configDialog.py so it can access all EditorWindow instaces
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000082 self.top.instance_dict=flist.inversedict
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000083 else:
84 self.tkinter_vars = {} # keys: Tkinter event names
85 # values: Tkinter variable instances
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000086 self.recent_files_path=os.path.join(idleConf.GetUserCfgDir(),
Steven M. Gava1d46e402002-03-27 08:40:46 +000087 'recent-files.lst')
David Scherer7aced172000-08-15 01:13:23 +000088 self.vbar = vbar = Scrollbar(top, name='vbar')
89 self.text_frame = text_frame = Frame(top)
Kurt B. Kaiser1061e722003-01-04 01:43:53 +000090 self.width = idleConf.GetOption('main','EditorWindow','width')
Kurt B. Kaisera1dee062002-10-04 21:33:57 +000091 self.text = text = Text(text_frame, name='text', padx=5, wrap='none',
Steven M. Gavadc72f482002-01-03 11:51:07 +000092 foreground=idleConf.GetHighlight(currentTheme,
93 'normal',fgBg='fg'),
94 background=idleConf.GetHighlight(currentTheme,
95 'normal',fgBg='bg'),
96 highlightcolor=idleConf.GetHighlight(currentTheme,
97 'hilite',fgBg='fg'),
98 highlightbackground=idleConf.GetHighlight(currentTheme,
99 'hilite',fgBg='bg'),
100 insertbackground=idleConf.GetHighlight(currentTheme,
101 'cursor',fgBg='fg'),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000102 width=self.width,
Steven M. Gavadc72f482002-01-03 11:51:07 +0000103 height=idleConf.GetOption('main','EditorWindow','height') )
David Scherer7aced172000-08-15 01:13:23 +0000104
105 self.createmenubar()
106 self.apply_bindings()
107
108 self.top.protocol("WM_DELETE_WINDOW", self.close)
109 self.top.bind("<<close-window>>", self.close_event)
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000110 text.bind("<<cut>>", self.cut)
111 text.bind("<<copy>>", self.copy)
112 text.bind("<<paste>>", self.paste)
David Scherer7aced172000-08-15 01:13:23 +0000113 text.bind("<<center-insert>>", self.center_insert_event)
114 text.bind("<<help>>", self.help_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000115 text.bind("<<python-docs>>", self.python_docs)
116 text.bind("<<about-idle>>", self.about_dialog)
Steven M. Gava3b55a892001-11-21 05:56:26 +0000117 text.bind("<<open-config-dialog>>", self.config_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000118 text.bind("<<open-module>>", self.open_module)
119 text.bind("<<do-nothing>>", lambda event: "break")
120 text.bind("<<select-all>>", self.select_all)
121 text.bind("<<remove-selection>>", self.remove_selection)
Steven M. Gavac5976402002-01-04 03:06:08 +0000122 text.bind("<<find>>", self.find_event)
123 text.bind("<<find-again>>", self.find_again_event)
124 text.bind("<<find-in-files>>", self.find_in_files_event)
125 text.bind("<<find-selection>>", self.find_selection_event)
126 text.bind("<<replace>>", self.replace_event)
127 text.bind("<<goto-line>>", self.goto_line_event)
David Scherer7aced172000-08-15 01:13:23 +0000128 text.bind("<3>", self.right_menu_event)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000129 text.bind("<<smart-backspace>>",self.smart_backspace_event)
130 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
131 text.bind("<<smart-indent>>",self.smart_indent_event)
132 text.bind("<<indent-region>>",self.indent_region_event)
133 text.bind("<<dedent-region>>",self.dedent_region_event)
134 text.bind("<<comment-region>>",self.comment_region_event)
135 text.bind("<<uncomment-region>>",self.uncomment_region_event)
136 text.bind("<<tabify-region>>",self.tabify_region_event)
137 text.bind("<<untabify-region>>",self.untabify_region_event)
138 text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
139 text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000140 text.bind("<Left>", self.move_at_edge_if_selection(0))
141 text.bind("<Right>", self.move_at_edge_if_selection(1))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000142
David Scherer7aced172000-08-15 01:13:23 +0000143 if flist:
144 flist.inversedict[self] = key
145 if key:
146 flist.dict[key] = self
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000147 text.bind("<<open-new-window>>", self.new_callback)
David Scherer7aced172000-08-15 01:13:23 +0000148 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
149 text.bind("<<open-class-browser>>", self.open_class_browser)
150 text.bind("<<open-path-browser>>", self.open_path_browser)
151
Steven M. Gava898a3652001-10-07 11:10:44 +0000152 self.set_status_bar()
David Scherer7aced172000-08-15 01:13:23 +0000153 vbar['command'] = text.yview
154 vbar.pack(side=RIGHT, fill=Y)
David Scherer7aced172000-08-15 01:13:23 +0000155 text['yscrollcommand'] = vbar.set
Steven M. Gavab1585412002-03-12 00:21:56 +0000156 fontWeight='normal'
157 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
158 fontWeight='bold'
Steven M. Gavadc72f482002-01-03 11:51:07 +0000159 text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
Steven M. Gavab1585412002-03-12 00:21:56 +0000160 idleConf.GetOption('main','EditorWindow','font-size'),
161 fontWeight))
David Scherer7aced172000-08-15 01:13:23 +0000162 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
163 text.pack(side=TOP, fill=BOTH, expand=1)
164 text.focus_set()
165
166 self.per = per = self.Percolator(text)
167 if self.ispythonsource(filename):
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000168 self.color = color = self.ColorDelegator()
169 per.insertfilter(color)
David Scherer7aced172000-08-15 01:13:23 +0000170 else:
David Scherer7aced172000-08-15 01:13:23 +0000171 self.color = None
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000172
173 self.undo = undo = self.UndoDelegator()
174 per.insertfilter(undo)
175 text.undo_block_start = undo.undo_block_start
176 text.undo_block_stop = undo.undo_block_stop
177 undo.set_saved_change_hook(self.saved_change_hook)
178
179 # IOBinding implements file I/O and printing functionality
David Scherer7aced172000-08-15 01:13:23 +0000180 self.io = io = self.IOBinding(self)
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000181 io.set_filename_change_hook(self.filename_change_hook)
182
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000183 # Create the recent files submenu
184 self.recent_files_menu = Menu(self.menubar)
185 self.menudict['file'].insert_cascade(3, label='Recent Files',
186 underline=0,
187 menu=self.recent_files_menu)
188 self.update_recent_files_list()
David Scherer7aced172000-08-15 01:13:23 +0000189
David Scherer7aced172000-08-15 01:13:23 +0000190 if filename:
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000191 if os.path.exists(filename) and not os.path.isdir(filename):
David Scherer7aced172000-08-15 01:13:23 +0000192 io.loadfile(filename)
193 else:
194 io.set_filename(filename)
David Scherer7aced172000-08-15 01:13:23 +0000195 self.saved_change_hook()
196
197 self.load_extensions()
198
199 menu = self.menudict.get('windows')
200 if menu:
201 end = menu.index("end")
202 if end is None:
203 end = -1
204 if end >= 0:
205 menu.add_separator()
206 end = end + 1
207 self.wmenu_end = end
208 WindowList.register_callback(self.postwindowsmenu)
209
210 # Some abstractions so IDLE extensions are cross-IDE
211 self.askyesno = tkMessageBox.askyesno
212 self.askinteger = tkSimpleDialog.askinteger
213 self.showerror = tkMessageBox.showerror
214
215 if self.extensions.has_key('AutoIndent'):
216 self.extensions['AutoIndent'].set_indentation_params(
217 self.ispythonsource(filename))
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000218
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000219 def new_callback(self, event):
220 dirname, basename = self.io.defaultfilename()
221 self.flist.new(dirname)
222 return "break"
223
David Scherer7aced172000-08-15 01:13:23 +0000224 def set_status_bar(self):
Steven M. Gava898a3652001-10-07 11:10:44 +0000225 self.status_bar = self.MultiStatusBar(self.top)
David Scherer7aced172000-08-15 01:13:23 +0000226 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
227 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
228 self.status_bar.pack(side=BOTTOM, fill=X)
229 self.text.bind('<KeyRelease>', self.set_line_and_column)
230 self.text.bind('<ButtonRelease>', self.set_line_and_column)
231 self.text.after_idle(self.set_line_and_column)
232
233 def set_line_and_column(self, event=None):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000234 line, column = self.text.index(INSERT).split('.')
David Scherer7aced172000-08-15 01:13:23 +0000235 self.status_bar.set_label('column', 'Col: %s' % column)
236 self.status_bar.set_label('line', 'Ln: %s' % line)
237
238 def wakeup(self):
239 if self.top.wm_state() == "iconic":
240 self.top.wm_deiconify()
241 else:
242 self.top.tkraise()
243 self.text.focus_set()
244
245 menu_specs = [
246 ("file", "_File"),
247 ("edit", "_Edit"),
248 ("format", "F_ormat"),
249 ("run", "_Run"),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000250 ("options", "_Options"),
David Scherer7aced172000-08-15 01:13:23 +0000251 ("windows", "_Windows"),
252 ("help", "_Help"),
253 ]
254
255 def createmenubar(self):
256 mbar = self.menubar
257 self.menudict = menudict = {}
258 for name, label in self.menu_specs:
259 underline, label = prepstr(label)
260 menudict[name] = menu = Menu(mbar, name=name)
261 mbar.add_cascade(label=label, menu=menu, underline=underline)
262 self.fill_menus()
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000263 self.base_helpmenu_length = self.menudict['help'].index(END)
264 self.reset_help_menu_entries()
David Scherer7aced172000-08-15 01:13:23 +0000265
266 def postwindowsmenu(self):
267 # Only called when Windows menu exists
David Scherer7aced172000-08-15 01:13:23 +0000268 menu = self.menudict['windows']
269 end = menu.index("end")
270 if end is None:
271 end = -1
272 if end > self.wmenu_end:
273 menu.delete(self.wmenu_end+1, end)
274 WindowList.add_windows_to_menu(menu)
275
276 rmenu = None
277
278 def right_menu_event(self, event):
279 self.text.tag_remove("sel", "1.0", "end")
280 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
281 if not self.rmenu:
282 self.make_rmenu()
283 rmenu = self.rmenu
284 self.event = event
285 iswin = sys.platform[:3] == 'win'
286 if iswin:
287 self.text.config(cursor="arrow")
288 rmenu.tk_popup(event.x_root, event.y_root)
289 if iswin:
290 self.text.config(cursor="ibeam")
291
292 rmenu_specs = [
293 # ("Label", "<<virtual-event>>"), ...
294 ("Close", "<<close-window>>"), # Example
295 ]
296
297 def make_rmenu(self):
298 rmenu = Menu(self.text, tearoff=0)
299 for label, eventname in self.rmenu_specs:
300 def command(text=self.text, eventname=eventname):
301 text.event_generate(eventname)
302 rmenu.add_command(label=label, command=command)
303 self.rmenu = rmenu
304
305 def about_dialog(self, event=None):
Kurt B. Kaiserd78b2302003-06-12 04:03:49 +0000306 aboutDialog.AboutDialog(self.top,'About IDLE')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000307
Steven M. Gava3b55a892001-11-21 05:56:26 +0000308 def config_dialog(self, event=None):
309 configDialog.ConfigDialog(self.top,'Settings')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000310
David Scherer7aced172000-08-15 01:13:23 +0000311 def help_dialog(self, event=None):
Steven M. Gavab9d07b52001-07-31 11:11:38 +0000312 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000313 textView.TextViewer(self.top,'Help',fn)
314
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000315 def python_docs(self, event=None):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000316 if sys.platform[:3] == 'win':
Steven M. Gava931625d2002-04-22 00:38:26 +0000317 os.startfile(self.help_url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000318 else:
319 webbrowser.open(self.help_url)
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000320 return "break"
David Scherer7aced172000-08-15 01:13:23 +0000321
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000322 def cut(self,event):
323 self.text.event_generate("<<Cut>>")
324 return "break"
325
326 def copy(self,event):
327 self.text.event_generate("<<Copy>>")
328 return "break"
329
330 def paste(self,event):
331 self.text.event_generate("<<Paste>>")
332 return "break"
333
David Scherer7aced172000-08-15 01:13:23 +0000334 def select_all(self, event=None):
335 self.text.tag_add("sel", "1.0", "end-1c")
336 self.text.mark_set("insert", "1.0")
337 self.text.see("insert")
338 return "break"
339
340 def remove_selection(self, event=None):
341 self.text.tag_remove("sel", "1.0", "end")
342 self.text.see("insert")
343
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000344 def move_at_edge_if_selection(self, edge_index):
345 """Cursor move begins at start or end of selection
346
347 When a left/right cursor key is pressed create and return to Tkinter a
348 function which causes a cursor move from the associated edge of the
349 selection.
350
351 """
352 self_text_index = self.text.index
353 self_text_mark_set = self.text.mark_set
354 edges_table = ("sel.first+1c", "sel.last-1c")
355 def move_at_edge(event):
356 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
357 try:
358 self_text_index("sel.first")
359 self_text_mark_set("insert", edges_table[edge_index])
360 except TclError:
361 pass
362 return move_at_edge
363
Steven M. Gavac5976402002-01-04 03:06:08 +0000364 def find_event(self, event):
365 SearchDialog.find(self.text)
366 return "break"
367
368 def find_again_event(self, event):
369 SearchDialog.find_again(self.text)
370 return "break"
371
372 def find_selection_event(self, event):
373 SearchDialog.find_selection(self.text)
374 return "break"
375
376 def find_in_files_event(self, event):
377 GrepDialog.grep(self.text, self.io, self.flist)
378 return "break"
379
380 def replace_event(self, event):
381 ReplaceDialog.replace(self.text)
382 return "break"
383
384 def goto_line_event(self, event):
385 text = self.text
386 lineno = tkSimpleDialog.askinteger("Goto",
387 "Go to line number:",parent=text)
388 if lineno is None:
389 return "break"
390 if lineno <= 0:
391 text.bell()
392 return "break"
393 text.mark_set("insert", "%d.0" % lineno)
394 text.see("insert")
395
David Scherer7aced172000-08-15 01:13:23 +0000396 def open_module(self, event=None):
397 # XXX Shouldn't this be in IOBinding or in FileList?
398 try:
399 name = self.text.get("sel.first", "sel.last")
400 except TclError:
401 name = ""
402 else:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000403 name = name.strip()
Guido van Rossum852f35b2003-06-05 11:36:55 +0000404 name = tkSimpleDialog.askstring("Module",
405 "Enter the name of a Python module\n"
406 "to search on sys.path and open:",
407 parent=self.text, initialvalue=name)
408 if name:
409 name = name.strip()
David Scherer7aced172000-08-15 01:13:23 +0000410 if not name:
Guido van Rossum852f35b2003-06-05 11:36:55 +0000411 return
David Scherer7aced172000-08-15 01:13:23 +0000412 # XXX Ought to insert current file's directory in front of path
413 try:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000414 (f, file, (suffix, mode, type)) = _find_module(name)
David Scherer7aced172000-08-15 01:13:23 +0000415 except (NameError, ImportError), msg:
416 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
417 return
418 if type != imp.PY_SOURCE:
419 tkMessageBox.showerror("Unsupported type",
420 "%s is not a source module" % name, parent=self.text)
421 return
422 if f:
423 f.close()
424 if self.flist:
425 self.flist.open(file)
426 else:
427 self.io.loadfile(file)
428
429 def open_class_browser(self, event=None):
430 filename = self.io.filename
431 if not filename:
432 tkMessageBox.showerror(
433 "No filename",
434 "This buffer has no associated filename",
435 master=self.text)
436 self.text.focus_set()
437 return None
438 head, tail = os.path.split(filename)
439 base, ext = os.path.splitext(tail)
440 import ClassBrowser
441 ClassBrowser.ClassBrowser(self.flist, base, [head])
442
443 def open_path_browser(self, event=None):
444 import PathBrowser
445 PathBrowser.PathBrowser(self.flist)
446
447 def gotoline(self, lineno):
448 if lineno is not None and lineno > 0:
449 self.text.mark_set("insert", "%d.0" % lineno)
450 self.text.tag_remove("sel", "1.0", "end")
451 self.text.tag_add("sel", "insert", "insert +1l")
452 self.center()
453
454 def ispythonsource(self, filename):
455 if not filename:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000456 return True
David Scherer7aced172000-08-15 01:13:23 +0000457 base, ext = os.path.splitext(os.path.basename(filename))
458 if os.path.normcase(ext) in (".py", ".pyw"):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000459 return True
David Scherer7aced172000-08-15 01:13:23 +0000460 try:
461 f = open(filename)
462 line = f.readline()
463 f.close()
464 except IOError:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000465 return False
Kurt B. Kaiser83a35602002-12-20 17:18:03 +0000466 return line.startswith('#!') and line.find('python') >= 0
David Scherer7aced172000-08-15 01:13:23 +0000467
468 def close_hook(self):
469 if self.flist:
470 self.flist.close_edit(self)
471
472 def set_close_hook(self, close_hook):
473 self.close_hook = close_hook
474
475 def filename_change_hook(self):
476 if self.flist:
477 self.flist.filename_changed_edit(self)
478 self.saved_change_hook()
Kurt B. Kaiser260cb902003-06-06 21:58:38 +0000479 self.top.update_windowlist_registry(self)
David Scherer7aced172000-08-15 01:13:23 +0000480 if self.ispythonsource(self.io.filename):
481 self.addcolorizer()
482 else:
483 self.rmcolorizer()
484
485 def addcolorizer(self):
486 if self.color:
487 return
David Scherer7aced172000-08-15 01:13:23 +0000488 self.per.removefilter(self.undo)
489 self.color = self.ColorDelegator()
490 self.per.insertfilter(self.color)
491 self.per.insertfilter(self.undo)
492
493 def rmcolorizer(self):
494 if not self.color:
495 return
David Scherer7aced172000-08-15 01:13:23 +0000496 self.per.removefilter(self.undo)
497 self.per.removefilter(self.color)
498 self.color = None
499 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000500
Steven M. Gavab77d3432002-03-02 07:16:21 +0000501 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000502 "Update the colour theme if it is changed"
503 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000504 if self.color:
505 self.color = self.ColorDelegator()
506 self.per.insertfilter(self.color)
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000507 theme = idleConf.GetOption('main','Theme','name')
508 self.text.config(idleConf.GetHighlight(theme, "normal"))
David Scherer7aced172000-08-15 01:13:23 +0000509
Steven M. Gavab1585412002-03-12 00:21:56 +0000510 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000511 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000512 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000513 fontWeight='normal'
514 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
515 fontWeight='bold'
516 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
517 idleConf.GetOption('main','EditorWindow','font-size'),
518 fontWeight))
519
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000520 def ResetKeybindings(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000521 "Update the keybindings if they are changed"
522 # Called from configDialog.py
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000523 self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
524 keydefs = self.Bindings.default_keydefs
525 for event, keylist in keydefs.items():
526 self.text.event_delete(event)
527 self.apply_bindings()
528 #update menu accelerators
529 menuEventDict={}
530 for menu in self.Bindings.menudefs:
531 menuEventDict[menu[0]]={}
532 for item in menu[1]:
533 if item:
534 menuEventDict[menu[0]][prepstr(item[0])[1]]=item[1]
535 for menubarItem in self.menudict.keys():
536 menu=self.menudict[menubarItem]
537 end=menu.index(END)+1
538 for index in range(0,end):
539 if menu.type(index)=='command':
540 accel=menu.entrycget(index,'accelerator')
541 if accel:
542 itemName=menu.entrycget(index,'label')
543 event=''
544 if menuEventDict.has_key(menubarItem):
545 if menuEventDict[menubarItem].has_key(itemName):
546 event=menuEventDict[menubarItem][itemName]
547 if event:
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000548 accel=get_accelerator(keydefs, event)
549 menu.entryconfig(index,accelerator=accel)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000550
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000551 def reset_help_menu_entries(self):
552 "Update the additional help entries on the Help menu"
553 help_list = idleConf.GetAllExtraHelpSourcesList()
554 helpmenu = self.menudict['help']
555 # first delete the extra help entries, if any
556 helpmenu_length = helpmenu.index(END)
557 if helpmenu_length > self.base_helpmenu_length:
558 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
559 # then rebuild them
560 if help_list:
561 helpmenu.add_separator()
562 for entry in help_list:
563 cmd = self.__extra_help_callback(entry[1])
564 helpmenu.add_command(label=entry[0], command=cmd)
565 # and update the menu dictionary
566 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000567
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000568 def __extra_help_callback(self, helpfile):
569 "Create a callback with the helpfile value frozen at definition time"
570 def display_extra_help(helpfile=helpfile):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000571 if not (helpfile.startswith('www') or helpfile.startswith('http')):
572 url = os.path.normpath(helpfile)
573 if sys.platform[:3] == 'win':
574 os.startfile(helpfile)
575 else:
576 webbrowser.open(helpfile)
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000577 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000578
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000579 def update_recent_files_list(self, new_file=None):
580 "Load and update the recent files list and menus"
581 rf_list = []
582 if os.path.exists(self.recent_files_path):
583 rf_list_file = open(self.recent_files_path,'r')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000584 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000585 rf_list = rf_list_file.readlines()
Steven M. Gava1d46e402002-03-27 08:40:46 +0000586 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000587 rf_list_file.close()
588 if new_file:
589 new_file = os.path.abspath(new_file) + '\n'
590 if new_file in rf_list:
591 rf_list.remove(new_file) # move to top
592 rf_list.insert(0, new_file)
593 # clean and save the recent files list
594 bad_paths = []
595 for path in rf_list:
596 if '\0' in path or not os.path.exists(path[0:-1]):
597 bad_paths.append(path)
598 rf_list = [path for path in rf_list if path not in bad_paths]
599 ulchars = "1234567890ABCDEFGHIJK"
600 rf_list = rf_list[0:len(ulchars)]
601 rf_file = open(self.recent_files_path, 'w')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000602 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000603 rf_file.writelines(rf_list)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000604 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000605 rf_file.close()
606 # for each edit window instance, construct the recent files menu
607 for instance in self.top.instance_dict.keys():
608 menu = instance.recent_files_menu
609 menu.delete(1, END) # clear, and rebuild:
610 for i, file in zip(count(), rf_list):
611 file_name = file[0:-1] # zap \n
612 callback = instance.__recent_file_callback(file_name)
613 menu.add_command(label=ulchars[i] + " " + file_name,
614 command=callback,
615 underline=0)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000616
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000617 def __recent_file_callback(self, file_name):
618 def open_recent_file(fn_closure=file_name):
619 self.io.open(editFile=fn_closure)
620 return open_recent_file
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000621
David Scherer7aced172000-08-15 01:13:23 +0000622 def saved_change_hook(self):
623 short = self.short_title()
624 long = self.long_title()
625 if short and long:
626 title = short + " - " + long
627 elif short:
628 title = short
629 elif long:
630 title = long
631 else:
632 title = "Untitled"
633 icon = short or long or title
634 if not self.get_saved():
635 title = "*%s*" % title
636 icon = "*%s" % icon
637 self.top.wm_title(title)
638 self.top.wm_iconname(icon)
639
640 def get_saved(self):
641 return self.undo.get_saved()
642
643 def set_saved(self, flag):
644 self.undo.set_saved(flag)
645
646 def reset_undo(self):
647 self.undo.reset_undo()
648
649 def short_title(self):
650 filename = self.io.filename
651 if filename:
652 filename = os.path.basename(filename)
653 return filename
654
655 def long_title(self):
656 return self.io.filename or ""
657
658 def center_insert_event(self, event):
659 self.center()
660
661 def center(self, mark="insert"):
662 text = self.text
663 top, bot = self.getwindowlines()
664 lineno = self.getlineno(mark)
665 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000666 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000667 text.yview(float(newtop))
668
669 def getwindowlines(self):
670 text = self.text
671 top = self.getlineno("@0,0")
672 bot = self.getlineno("@0,65535")
673 if top == bot and text.winfo_height() == 1:
674 # Geometry manager hasn't run yet
675 height = int(text['height'])
676 bot = top + height - 1
677 return top, bot
678
679 def getlineno(self, mark="insert"):
680 text = self.text
681 return int(float(text.index(mark)))
682
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000683 def get_geometry(self):
684 "Return (width, height, x, y)"
685 geom = self.top.wm_geometry()
686 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
687 tuple = (map(int, m.groups()))
688 return tuple
689
David Scherer7aced172000-08-15 01:13:23 +0000690 def close_event(self, event):
691 self.close()
692
693 def maybesave(self):
694 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000695 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000696 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000697 self.top.deiconify()
698 self.top.lower()
699 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000700 return self.io.maybesave()
701
702 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000703 reply = self.maybesave()
704 if reply != "cancel":
705 self._close()
706 return reply
707
708 def _close(self):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000709 if self.io.filename:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000710 self.update_recent_files_list(new_file=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000711 WindowList.unregister_callback(self.postwindowsmenu)
712 if self.close_hook:
713 self.close_hook()
714 self.flist = None
715 colorizing = 0
716 self.unload_extensions()
717 self.io.close(); self.io = None
718 self.undo = None # XXX
719 if self.color:
720 colorizing = self.color.colorizing
721 doh = colorizing and self.top
722 self.color.close(doh) # Cancel colorization
723 self.text = None
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000724 self.tkinter_vars = None
David Scherer7aced172000-08-15 01:13:23 +0000725 self.per.close(); self.per = None
726 if not colorizing:
727 self.top.destroy()
728
729 def load_extensions(self):
730 self.extensions = {}
731 self.load_standard_extensions()
732
733 def unload_extensions(self):
734 for ins in self.extensions.values():
735 if hasattr(ins, "close"):
736 ins.close()
737 self.extensions = {}
738
739 def load_standard_extensions(self):
740 for name in self.get_standard_extension_names():
741 try:
742 self.load_extension(name)
743 except:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000744 print "Failed to load extension", repr(name)
David Scherer7aced172000-08-15 01:13:23 +0000745 import traceback
746 traceback.print_exc()
747
748 def get_standard_extension_names(self):
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000749 return idleConf.GetExtensions(editor_only=True)
David Scherer7aced172000-08-15 01:13:23 +0000750
751 def load_extension(self, name):
752 mod = __import__(name, globals(), locals(), [])
753 cls = getattr(mod, name)
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000754 keydefs = idleConf.GetExtensionBindings(name)
755 if hasattr(cls, "menudefs"):
756 self.fill_menus(cls.menudefs, keydefs)
David Scherer7aced172000-08-15 01:13:23 +0000757 ins = cls(self)
758 self.extensions[name] = ins
David Scherer7aced172000-08-15 01:13:23 +0000759 if keydefs:
760 self.apply_bindings(keydefs)
761 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000762 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000763 while methodname[:1] == '<':
764 methodname = methodname[1:]
765 while methodname[-1:] == '>':
766 methodname = methodname[:-1]
767 methodname = methodname + "_event"
768 if hasattr(ins, methodname):
769 self.text.bind(vevent, getattr(ins, methodname))
David Scherer7aced172000-08-15 01:13:23 +0000770 return ins
771
772 def apply_bindings(self, keydefs=None):
773 if keydefs is None:
774 keydefs = self.Bindings.default_keydefs
775 text = self.text
776 text.keydefs = keydefs
777 for event, keylist in keydefs.items():
778 if keylist:
Raymond Hettinger931237e2003-07-09 18:48:24 +0000779 text.event_add(event, *keylist)
David Scherer7aced172000-08-15 01:13:23 +0000780
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000781 def fill_menus(self, menudefs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000782 """Add appropriate entries to the menus and submenus
783
784 Menus that are absent or None in self.menudict are ignored.
785 """
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000786 if menudefs is None:
787 menudefs = self.Bindings.menudefs
David Scherer7aced172000-08-15 01:13:23 +0000788 if keydefs is None:
789 keydefs = self.Bindings.default_keydefs
790 menudict = self.menudict
791 text = self.text
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000792 for mname, entrylist in menudefs:
David Scherer7aced172000-08-15 01:13:23 +0000793 menu = menudict.get(mname)
794 if not menu:
795 continue
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000796 for entry in entrylist:
797 if not entry:
David Scherer7aced172000-08-15 01:13:23 +0000798 menu.add_separator()
799 else:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000800 label, eventname = entry
David Scherer7aced172000-08-15 01:13:23 +0000801 checkbutton = (label[:1] == '!')
802 if checkbutton:
803 label = label[1:]
804 underline, label = prepstr(label)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000805 accelerator = get_accelerator(keydefs, eventname)
806 def command(text=text, eventname=eventname):
807 text.event_generate(eventname)
David Scherer7aced172000-08-15 01:13:23 +0000808 if checkbutton:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000809 var = self.get_var_obj(eventname, BooleanVar)
David Scherer7aced172000-08-15 01:13:23 +0000810 menu.add_checkbutton(label=label, underline=underline,
811 command=command, accelerator=accelerator,
812 variable=var)
813 else:
814 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000815 command=command,
816 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000817
818 def getvar(self, name):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000819 var = self.get_var_obj(name)
David Scherer7aced172000-08-15 01:13:23 +0000820 if var:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000821 value = var.get()
822 return value
823 else:
824 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000825
826 def setvar(self, name, value, vartype=None):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000827 var = self.get_var_obj(name, vartype)
David Scherer7aced172000-08-15 01:13:23 +0000828 if var:
829 var.set(value)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000830 else:
831 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000832
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000833 def get_var_obj(self, name, vartype=None):
834 var = self.tkinter_vars.get(name)
David Scherer7aced172000-08-15 01:13:23 +0000835 if not var and vartype:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000836 # create a Tkinter variable object with self.text as master:
837 self.tkinter_vars[name] = var = vartype(self.text)
David Scherer7aced172000-08-15 01:13:23 +0000838 return var
839
840 # Tk implementations of "virtual text methods" -- each platform
841 # reusing IDLE's support code needs to define these for its GUI's
842 # flavor of widget.
843
844 # Is character at text_index in a Python string? Return 0 for
845 # "guaranteed no", true for anything else. This info is expensive
846 # to compute ab initio, but is probably already known by the
847 # platform's colorizer.
848
849 def is_char_in_string(self, text_index):
850 if self.color:
851 # Return true iff colorizer hasn't (re)gotten this far
852 # yet, or the character is tagged as being in a string
853 return self.text.tag_prevrange("TODO", text_index) or \
854 "STRING" in self.text.tag_names(text_index)
855 else:
856 # The colorizer is missing: assume the worst
857 return 1
858
859 # If a selection is defined in the text widget, return (start,
860 # end) as Tkinter text indices, otherwise return (None, None)
861 def get_selection_indices(self):
862 try:
863 first = self.text.index("sel.first")
864 last = self.text.index("sel.last")
865 return first, last
866 except TclError:
867 return None, None
868
869 # Return the text widget's current view of what a tab stop means
870 # (equivalent width in spaces).
871
872 def get_tabwidth(self):
873 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
874 return int(current)
875
876 # Set the text widget's current view of what a tab stop means.
877
878 def set_tabwidth(self, newtabwidth):
879 text = self.text
880 if self.get_tabwidth() != newtabwidth:
881 pixels = text.tk.call("font", "measure", text["font"],
882 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000883 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000884 text.configure(tabs=pixels)
885
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000886### begin autoindent code ###
887
888 # usetabs true -> literal tab characters are used by indent and
889 # dedent cmds, possibly mixed with spaces if
890 # indentwidth is not a multiple of tabwidth
891 # false -> tab characters are converted to spaces by indent
892 # and dedent cmds, and ditto TAB keystrokes
893 # indentwidth is the number of characters per logical indent level.
894 # tabwidth is the display width of a literal tab character.
895 # CAUTION: telling Tk to use anything other than its default
896 # tab setting causes it to use an entirely different tabbing algorithm,
897 # treating tab stops as fixed distances from the left margin.
898 # Nobody expects this, so for now tabwidth should never be changed.
899 usetabs = 0
900 indentwidth = 4
901 tabwidth = 8 # for IDLE use, must remain 8 until Tk is fixed
902
903 # If context_use_ps1 is true, parsing searches back for a ps1 line;
904 # else searches for a popular (if, def, ...) Python stmt.
905 context_use_ps1 = 0
906
907 # When searching backwards for a reliable place to begin parsing,
908 # first start num_context_lines[0] lines back, then
909 # num_context_lines[1] lines back if that didn't work, and so on.
910 # The last value should be huge (larger than the # of lines in a
911 # conceivable file).
912 # Making the initial values larger slows things down more often.
913 num_context_lines = 50, 500, 5000000
914
915 def config(self, **options):
916 for key, value in options.items():
917 if key == 'usetabs':
918 self.usetabs = value
919 elif key == 'indentwidth':
920 self.indentwidth = value
921 elif key == 'tabwidth':
922 self.tabwidth = value
923 elif key == 'context_use_ps1':
924 self.context_use_ps1 = value
925 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000926 raise KeyError, "bad option name: %r" % (key,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000927
928 # If ispythonsource and guess are true, guess a good value for
929 # indentwidth based on file content (if possible), and if
930 # indentwidth != tabwidth set usetabs false.
931 # In any case, adjust the Text widget's view of what a tab
932 # character means.
933
934 def set_indentation_params(self, ispythonsource, guess=1):
935 if guess and ispythonsource:
936 i = self.guess_indent()
937 if 2 <= i <= 8:
938 self.indentwidth = i
939 if self.indentwidth != self.tabwidth:
940 self.usetabs = 0
941
942 self.set_tabwidth(self.tabwidth)
943
944 def smart_backspace_event(self, event):
945 text = self.text
946 first, last = self.get_selection_indices()
947 if first and last:
948 text.delete(first, last)
949 text.mark_set("insert", first)
950 return "break"
951 # Delete whitespace left, until hitting a real char or closest
952 # preceding virtual tab stop.
953 chars = text.get("insert linestart", "insert")
954 if chars == '':
955 if text.compare("insert", ">", "1.0"):
956 # easy: delete preceding newline
957 text.delete("insert-1c")
958 else:
959 text.bell() # at start of buffer
960 return "break"
961 if chars[-1] not in " \t":
962 # easy: delete preceding real char
963 text.delete("insert-1c")
964 return "break"
965 # Ick. It may require *inserting* spaces if we back up over a
966 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000967 tabwidth = self.tabwidth
968 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000969 assert have > 0
970 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000971 # Debug prompt is multilined....
972 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000973 ncharsdeleted = 0
974 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000975 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +0000976 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000977 chars = chars[:-1]
978 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000979 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000980 if have <= want or chars[-1] not in " \t":
981 break
982 text.undo_block_start()
983 text.delete("insert-%dc" % ncharsdeleted, "insert")
984 if have < want:
985 text.insert("insert", ' ' * (want - have))
986 text.undo_block_stop()
987 return "break"
988
989 def smart_indent_event(self, event):
990 # if intraline selection:
991 # delete it
992 # elif multiline selection:
993 # do indent-region & return
994 # indent one level
995 text = self.text
996 first, last = self.get_selection_indices()
997 text.undo_block_start()
998 try:
999 if first and last:
1000 if index2line(first) != index2line(last):
1001 return self.indent_region_event(event)
1002 text.delete(first, last)
1003 text.mark_set("insert", first)
1004 prefix = text.get("insert linestart", "insert")
1005 raw, effective = classifyws(prefix, self.tabwidth)
1006 if raw == len(prefix):
1007 # only whitespace to the left
1008 self.reindent_to(effective + self.indentwidth)
1009 else:
1010 if self.usetabs:
1011 pad = '\t'
1012 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001013 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001014 n = self.indentwidth
1015 pad = ' ' * (n - effective % n)
1016 text.insert("insert", pad)
1017 text.see("insert")
1018 return "break"
1019 finally:
1020 text.undo_block_stop()
1021
1022 def newline_and_indent_event(self, event):
1023 text = self.text
1024 first, last = self.get_selection_indices()
1025 text.undo_block_start()
1026 try:
1027 if first and last:
1028 text.delete(first, last)
1029 text.mark_set("insert", first)
1030 line = text.get("insert linestart", "insert")
1031 i, n = 0, len(line)
1032 while i < n and line[i] in " \t":
1033 i = i+1
1034 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001035 # the cursor is in or at leading indentation in a continuation
1036 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001037 text.insert("insert linestart", '\n')
1038 return "break"
1039 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001040 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001041 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001042 last_line_of_prompt = sys.ps1.split('\n')[-1]
1043 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001044 line = line[:-1]
1045 i = i+1
1046 if i:
1047 text.delete("insert - %d chars" % i, "insert")
1048 # strip whitespace after insert point
1049 while text.get("insert") in " \t":
1050 text.delete("insert")
1051 # start new line
1052 text.insert("insert", '\n')
1053
1054 # adjust indentation for continuations and block
1055 # open/close first need to find the last stmt
1056 lno = index2line(text.index('insert'))
1057 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1058 for context in self.num_context_lines:
1059 startat = max(lno - context, 1)
Walter Dörwald70a6b492004-02-12 17:35:32 +00001060 startatindex = repr(startat) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001061 rawtext = text.get(startatindex, "insert")
1062 y.set_str(rawtext)
1063 bod = y.find_good_parse_start(
1064 self.context_use_ps1,
1065 self._build_char_in_string_func(startatindex))
1066 if bod is not None or startat == 1:
1067 break
1068 y.set_lo(bod or 0)
1069 c = y.get_continuation_type()
1070 if c != PyParse.C_NONE:
1071 # The current stmt hasn't ended yet.
1072 if c == PyParse.C_STRING:
1073 # inside a string; just mimic the current indent
1074 text.insert("insert", indent)
1075 elif c == PyParse.C_BRACKET:
1076 # line up with the first (if any) element of the
1077 # last open bracket structure; else indent one
1078 # level beyond the indent of the line with the
1079 # last open bracket
1080 self.reindent_to(y.compute_bracket_indent())
1081 elif c == PyParse.C_BACKSLASH:
1082 # if more than one line in this stmt already, just
1083 # mimic the current indent; else if initial line
1084 # has a start on an assignment stmt, indent to
1085 # beyond leftmost =; else to beyond first chunk of
1086 # non-whitespace on initial line
1087 if y.get_num_lines_in_stmt() > 1:
1088 text.insert("insert", indent)
1089 else:
1090 self.reindent_to(y.compute_backslash_indent())
1091 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001092 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001093 return "break"
1094
1095 # This line starts a brand new stmt; indent relative to
1096 # indentation of initial line of closest preceding
1097 # interesting stmt.
1098 indent = y.get_base_indent_string()
1099 text.insert("insert", indent)
1100 if y.is_block_opener():
1101 self.smart_indent_event(event)
1102 elif indent and y.is_block_closer():
1103 self.smart_backspace_event(event)
1104 return "break"
1105 finally:
1106 text.see("insert")
1107 text.undo_block_stop()
1108
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001109 # Our editwin provides a is_char_in_string function that works
1110 # with a Tk text index, but PyParse only knows about offsets into
1111 # a string. This builds a function for PyParse that accepts an
1112 # offset.
1113
1114 def _build_char_in_string_func(self, startindex):
1115 def inner(offset, _startindex=startindex,
1116 _icis=self.is_char_in_string):
1117 return _icis(_startindex + "+%dc" % offset)
1118 return inner
1119
1120 def indent_region_event(self, event):
1121 head, tail, chars, lines = self.get_region()
1122 for pos in range(len(lines)):
1123 line = lines[pos]
1124 if line:
1125 raw, effective = classifyws(line, self.tabwidth)
1126 effective = effective + self.indentwidth
1127 lines[pos] = self._make_blanks(effective) + line[raw:]
1128 self.set_region(head, tail, chars, lines)
1129 return "break"
1130
1131 def dedent_region_event(self, event):
1132 head, tail, chars, lines = self.get_region()
1133 for pos in range(len(lines)):
1134 line = lines[pos]
1135 if line:
1136 raw, effective = classifyws(line, self.tabwidth)
1137 effective = max(effective - self.indentwidth, 0)
1138 lines[pos] = self._make_blanks(effective) + line[raw:]
1139 self.set_region(head, tail, chars, lines)
1140 return "break"
1141
1142 def comment_region_event(self, event):
1143 head, tail, chars, lines = self.get_region()
1144 for pos in range(len(lines) - 1):
1145 line = lines[pos]
1146 lines[pos] = '##' + line
1147 self.set_region(head, tail, chars, lines)
1148
1149 def uncomment_region_event(self, event):
1150 head, tail, chars, lines = self.get_region()
1151 for pos in range(len(lines)):
1152 line = lines[pos]
1153 if not line:
1154 continue
1155 if line[:2] == '##':
1156 line = line[2:]
1157 elif line[:1] == '#':
1158 line = line[1:]
1159 lines[pos] = line
1160 self.set_region(head, tail, chars, lines)
1161
1162 def tabify_region_event(self, event):
1163 head, tail, chars, lines = self.get_region()
1164 tabwidth = self._asktabwidth()
1165 for pos in range(len(lines)):
1166 line = lines[pos]
1167 if line:
1168 raw, effective = classifyws(line, tabwidth)
1169 ntabs, nspaces = divmod(effective, tabwidth)
1170 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1171 self.set_region(head, tail, chars, lines)
1172
1173 def untabify_region_event(self, event):
1174 head, tail, chars, lines = self.get_region()
1175 tabwidth = self._asktabwidth()
1176 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001177 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001178 self.set_region(head, tail, chars, lines)
1179
1180 def toggle_tabs_event(self, event):
1181 if self.askyesno(
1182 "Toggle tabs",
1183 "Turn tabs " + ("on", "off")[self.usetabs] + "?",
1184 parent=self.text):
1185 self.usetabs = not self.usetabs
1186 return "break"
1187
1188 # XXX this isn't bound to anything -- see class tabwidth comments
1189 def change_tabwidth_event(self, event):
1190 new = self._asktabwidth()
1191 if new != self.tabwidth:
1192 self.tabwidth = new
1193 self.set_indentation_params(0, guess=0)
1194 return "break"
1195
1196 def change_indentwidth_event(self, event):
1197 new = self.askinteger(
1198 "Indent width",
1199 "New indent width (2-16)",
1200 parent=self.text,
1201 initialvalue=self.indentwidth,
1202 minvalue=2,
1203 maxvalue=16)
1204 if new and new != self.indentwidth:
1205 self.indentwidth = new
1206 return "break"
1207
1208 def get_region(self):
1209 text = self.text
1210 first, last = self.get_selection_indices()
1211 if first and last:
1212 head = text.index(first + " linestart")
1213 tail = text.index(last + "-1c lineend +1c")
1214 else:
1215 head = text.index("insert linestart")
1216 tail = text.index("insert lineend +1c")
1217 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001218 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001219 return head, tail, chars, lines
1220
1221 def set_region(self, head, tail, chars, lines):
1222 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001223 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001224 if newchars == chars:
1225 text.bell()
1226 return
1227 text.tag_remove("sel", "1.0", "end")
1228 text.mark_set("insert", head)
1229 text.undo_block_start()
1230 text.delete(head, tail)
1231 text.insert(head, newchars)
1232 text.undo_block_stop()
1233 text.tag_add("sel", head, "insert")
1234
1235 # Make string that displays as n leading blanks.
1236
1237 def _make_blanks(self, n):
1238 if self.usetabs:
1239 ntabs, nspaces = divmod(n, self.tabwidth)
1240 return '\t' * ntabs + ' ' * nspaces
1241 else:
1242 return ' ' * n
1243
1244 # Delete from beginning of line to insert point, then reinsert
1245 # column logical (meaning use tabs if appropriate) spaces.
1246
1247 def reindent_to(self, column):
1248 text = self.text
1249 text.undo_block_start()
1250 if text.compare("insert linestart", "!=", "insert"):
1251 text.delete("insert linestart", "insert")
1252 if column:
1253 text.insert("insert", self._make_blanks(column))
1254 text.undo_block_stop()
1255
1256 def _asktabwidth(self):
1257 return self.askinteger(
1258 "Tab width",
1259 "Spaces per tab? (2-16)",
1260 parent=self.text,
1261 initialvalue=self.indentwidth,
1262 minvalue=2,
1263 maxvalue=16) or self.tabwidth
1264
1265 # Guess indentwidth from text content.
1266 # Return guessed indentwidth. This should not be believed unless
1267 # it's in a reasonable range (e.g., it will be 0 if no indented
1268 # blocks are found).
1269
1270 def guess_indent(self):
1271 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1272 if opener and indented:
1273 raw, indentsmall = classifyws(opener, self.tabwidth)
1274 raw, indentlarge = classifyws(indented, self.tabwidth)
1275 else:
1276 indentsmall = indentlarge = 0
1277 return indentlarge - indentsmall
1278
1279# "line.col" -> line, as an int
1280def index2line(index):
1281 return int(float(index))
1282
1283# Look at the leading whitespace in s.
1284# Return pair (# of leading ws characters,
1285# effective # of leading blanks after expanding
1286# tabs to width tabwidth)
1287
1288def classifyws(s, tabwidth):
1289 raw = effective = 0
1290 for ch in s:
1291 if ch == ' ':
1292 raw = raw + 1
1293 effective = effective + 1
1294 elif ch == '\t':
1295 raw = raw + 1
1296 effective = (effective // tabwidth + 1) * tabwidth
1297 else:
1298 break
1299 return raw, effective
1300
1301import tokenize
1302_tokenize = tokenize
1303del tokenize
1304
1305class IndentSearcher:
1306
1307 # .run() chews over the Text widget, looking for a block opener
1308 # and the stmt following it. Returns a pair,
1309 # (line containing block opener, line containing stmt)
1310 # Either or both may be None.
1311
1312 def __init__(self, text, tabwidth):
1313 self.text = text
1314 self.tabwidth = tabwidth
1315 self.i = self.finished = 0
1316 self.blkopenline = self.indentedline = None
1317
1318 def readline(self):
1319 if self.finished:
1320 return ""
1321 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001322 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001323 if self.text.compare(mark, ">=", "end"):
1324 return ""
1325 return self.text.get(mark, mark + " lineend+1c")
1326
1327 def tokeneater(self, type, token, start, end, line,
1328 INDENT=_tokenize.INDENT,
1329 NAME=_tokenize.NAME,
1330 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1331 if self.finished:
1332 pass
1333 elif type == NAME and token in OPENERS:
1334 self.blkopenline = line
1335 elif type == INDENT and self.blkopenline:
1336 self.indentedline = line
1337 self.finished = 1
1338
1339 def run(self):
1340 save_tabsize = _tokenize.tabsize
1341 _tokenize.tabsize = self.tabwidth
1342 try:
1343 try:
1344 _tokenize.tokenize(self.readline, self.tokeneater)
1345 except _tokenize.TokenError:
1346 # since we cut off the tokenizer early, we can trigger
1347 # spurious errors
1348 pass
1349 finally:
1350 _tokenize.tabsize = save_tabsize
1351 return self.blkopenline, self.indentedline
1352
1353### end autoindent code ###
1354
David Scherer7aced172000-08-15 01:13:23 +00001355def prepstr(s):
1356 # Helper to extract the underscore from a string, e.g.
1357 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001358 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001359 if i >= 0:
1360 s = s[:i] + s[i+1:]
1361 return i, s
1362
1363
1364keynames = {
1365 'bracketleft': '[',
1366 'bracketright': ']',
1367 'slash': '/',
1368}
1369
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001370def get_accelerator(keydefs, eventname):
1371 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001372 if not keylist:
1373 return ""
1374 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001375 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001376 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1377 s = re.sub("Key-", "", s)
1378 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1379 s = re.sub("Control-", "Ctrl-", s)
1380 s = re.sub("-", "+", s)
1381 s = re.sub("><", " ", s)
1382 s = re.sub("<", "", s)
1383 s = re.sub(">", "", s)
1384 return s
1385
1386
1387def fixwordbreaks(root):
1388 # Make sure that Tk's double-click and next/previous word
1389 # operations use our definition of a word (i.e. an identifier)
1390 tk = root.tk
1391 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1392 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1393 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1394
1395
1396def test():
1397 root = Tk()
1398 fixwordbreaks(root)
1399 root.withdraw()
1400 if sys.argv[1:]:
1401 filename = sys.argv[1]
1402 else:
1403 filename = None
1404 edit = EditorWindow(root=root, filename=filename)
1405 edit.set_close_hook(root.quit)
1406 root.mainloop()
1407 root.destroy()
1408
1409if __name__ == '__main__':
1410 test()