blob: cc38122c51ac2f967295422e2d33dff353524428 [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. Kaiser3069dbb2005-01-28 00:16:16 +0000144 text.bind("<<del-word-left>>", self.del_word_left)
145 text.bind("<<del-word-right>>", self.del_word_right)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000146
David Scherer7aced172000-08-15 01:13:23 +0000147 if flist:
148 flist.inversedict[self] = key
149 if key:
150 flist.dict[key] = self
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000151 text.bind("<<open-new-window>>", self.new_callback)
David Scherer7aced172000-08-15 01:13:23 +0000152 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
153 text.bind("<<open-class-browser>>", self.open_class_browser)
154 text.bind("<<open-path-browser>>", self.open_path_browser)
155
Steven M. Gava898a3652001-10-07 11:10:44 +0000156 self.set_status_bar()
David Scherer7aced172000-08-15 01:13:23 +0000157 vbar['command'] = text.yview
158 vbar.pack(side=RIGHT, fill=Y)
David Scherer7aced172000-08-15 01:13:23 +0000159 text['yscrollcommand'] = vbar.set
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000160 fontWeight = 'normal'
161 if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'):
Steven M. Gavab1585412002-03-12 00:21:56 +0000162 fontWeight='bold'
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000163 text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
164 idleConf.GetOption('main', 'EditorWindow', 'font-size'),
165 fontWeight))
David Scherer7aced172000-08-15 01:13:23 +0000166 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
167 text.pack(side=TOP, fill=BOTH, expand=1)
168 text.focus_set()
169
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000170 # usetabs true -> literal tab characters are used by indent and
171 # dedent cmds, possibly mixed with spaces if
172 # indentwidth is not a multiple of tabwidth,
173 # which will cause Tabnanny to nag!
174 # false -> tab characters are converted to spaces by indent
175 # and dedent cmds, and ditto TAB keystrokes
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000176 # Although use-spaces=0 can be configured manually in config-main.def,
177 # configuration of tabs v. spaces is not supported in the configuration
178 # dialog. IDLE promotes the preferred Python indentation: use spaces!
179 usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool')
180 self.usetabs = not usespaces
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000181
182 # tabwidth is the display width of a literal tab character.
183 # CAUTION: telling Tk to use anything other than its default
184 # tab setting causes it to use an entirely different tabbing algorithm,
185 # treating tab stops as fixed distances from the left margin.
186 # Nobody expects this, so for now tabwidth should never be changed.
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000187 self.tabwidth = 8 # must remain 8 until Tk is fixed.
188
189 # indentwidth is the number of screen characters per indent level.
190 # The recommended Python indentation is four spaces.
191 self.indentwidth = self.tabwidth
192 self.set_notabs_indentwidth()
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000193
194 # If context_use_ps1 is true, parsing searches back for a ps1 line;
195 # else searches for a popular (if, def, ...) Python stmt.
196 self.context_use_ps1 = False
197
198 # When searching backwards for a reliable place to begin parsing,
199 # first start num_context_lines[0] lines back, then
200 # num_context_lines[1] lines back if that didn't work, and so on.
201 # The last value should be huge (larger than the # of lines in a
202 # conceivable file).
203 # Making the initial values larger slows things down more often.
204 self.num_context_lines = 50, 500, 5000000
205
David Scherer7aced172000-08-15 01:13:23 +0000206 self.per = per = self.Percolator(text)
207 if self.ispythonsource(filename):
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000208 self.color = color = self.ColorDelegator()
209 per.insertfilter(color)
David Scherer7aced172000-08-15 01:13:23 +0000210 else:
David Scherer7aced172000-08-15 01:13:23 +0000211 self.color = None
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000212
213 self.undo = undo = self.UndoDelegator()
214 per.insertfilter(undo)
215 text.undo_block_start = undo.undo_block_start
216 text.undo_block_stop = undo.undo_block_stop
217 undo.set_saved_change_hook(self.saved_change_hook)
218
219 # IOBinding implements file I/O and printing functionality
David Scherer7aced172000-08-15 01:13:23 +0000220 self.io = io = self.IOBinding(self)
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000221 io.set_filename_change_hook(self.filename_change_hook)
222
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000223 # Create the recent files submenu
224 self.recent_files_menu = Menu(self.menubar)
225 self.menudict['file'].insert_cascade(3, label='Recent Files',
226 underline=0,
227 menu=self.recent_files_menu)
228 self.update_recent_files_list()
David Scherer7aced172000-08-15 01:13:23 +0000229
David Scherer7aced172000-08-15 01:13:23 +0000230 if filename:
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000231 if os.path.exists(filename) and not os.path.isdir(filename):
David Scherer7aced172000-08-15 01:13:23 +0000232 io.loadfile(filename)
233 else:
234 io.set_filename(filename)
David Scherer7aced172000-08-15 01:13:23 +0000235 self.saved_change_hook()
236
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000237 self.set_indentation_params(self.ispythonsource(filename))
238
David Scherer7aced172000-08-15 01:13:23 +0000239 self.load_extensions()
240
241 menu = self.menudict.get('windows')
242 if menu:
243 end = menu.index("end")
244 if end is None:
245 end = -1
246 if end >= 0:
247 menu.add_separator()
248 end = end + 1
249 self.wmenu_end = end
250 WindowList.register_callback(self.postwindowsmenu)
251
252 # Some abstractions so IDLE extensions are cross-IDE
253 self.askyesno = tkMessageBox.askyesno
254 self.askinteger = tkSimpleDialog.askinteger
255 self.showerror = tkMessageBox.showerror
256
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000257 def new_callback(self, event):
258 dirname, basename = self.io.defaultfilename()
259 self.flist.new(dirname)
260 return "break"
261
David Scherer7aced172000-08-15 01:13:23 +0000262 def set_status_bar(self):
Steven M. Gava898a3652001-10-07 11:10:44 +0000263 self.status_bar = self.MultiStatusBar(self.top)
David Scherer7aced172000-08-15 01:13:23 +0000264 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
265 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
266 self.status_bar.pack(side=BOTTOM, fill=X)
267 self.text.bind('<KeyRelease>', self.set_line_and_column)
268 self.text.bind('<ButtonRelease>', self.set_line_and_column)
269 self.text.after_idle(self.set_line_and_column)
270
271 def set_line_and_column(self, event=None):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000272 line, column = self.text.index(INSERT).split('.')
David Scherer7aced172000-08-15 01:13:23 +0000273 self.status_bar.set_label('column', 'Col: %s' % column)
274 self.status_bar.set_label('line', 'Ln: %s' % line)
275
David Scherer7aced172000-08-15 01:13:23 +0000276 menu_specs = [
277 ("file", "_File"),
278 ("edit", "_Edit"),
279 ("format", "F_ormat"),
280 ("run", "_Run"),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000281 ("options", "_Options"),
David Scherer7aced172000-08-15 01:13:23 +0000282 ("windows", "_Windows"),
283 ("help", "_Help"),
284 ]
285
286 def createmenubar(self):
287 mbar = self.menubar
288 self.menudict = menudict = {}
289 for name, label in self.menu_specs:
290 underline, label = prepstr(label)
291 menudict[name] = menu = Menu(mbar, name=name)
292 mbar.add_cascade(label=label, menu=menu, underline=underline)
293 self.fill_menus()
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000294 self.base_helpmenu_length = self.menudict['help'].index(END)
295 self.reset_help_menu_entries()
David Scherer7aced172000-08-15 01:13:23 +0000296
297 def postwindowsmenu(self):
298 # Only called when Windows menu exists
David Scherer7aced172000-08-15 01:13:23 +0000299 menu = self.menudict['windows']
300 end = menu.index("end")
301 if end is None:
302 end = -1
303 if end > self.wmenu_end:
304 menu.delete(self.wmenu_end+1, end)
305 WindowList.add_windows_to_menu(menu)
306
307 rmenu = None
308
309 def right_menu_event(self, event):
310 self.text.tag_remove("sel", "1.0", "end")
311 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
312 if not self.rmenu:
313 self.make_rmenu()
314 rmenu = self.rmenu
315 self.event = event
316 iswin = sys.platform[:3] == 'win'
317 if iswin:
318 self.text.config(cursor="arrow")
319 rmenu.tk_popup(event.x_root, event.y_root)
320 if iswin:
321 self.text.config(cursor="ibeam")
322
323 rmenu_specs = [
324 # ("Label", "<<virtual-event>>"), ...
325 ("Close", "<<close-window>>"), # Example
326 ]
327
328 def make_rmenu(self):
329 rmenu = Menu(self.text, tearoff=0)
330 for label, eventname in self.rmenu_specs:
331 def command(text=self.text, eventname=eventname):
332 text.event_generate(eventname)
333 rmenu.add_command(label=label, command=command)
334 self.rmenu = rmenu
335
336 def about_dialog(self, event=None):
Kurt B. Kaiserd78b2302003-06-12 04:03:49 +0000337 aboutDialog.AboutDialog(self.top,'About IDLE')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000338
Steven M. Gava3b55a892001-11-21 05:56:26 +0000339 def config_dialog(self, event=None):
340 configDialog.ConfigDialog(self.top,'Settings')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000341
David Scherer7aced172000-08-15 01:13:23 +0000342 def help_dialog(self, event=None):
Steven M. Gavab9d07b52001-07-31 11:11:38 +0000343 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000344 textView.TextViewer(self.top,'Help',fn)
345
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000346 def python_docs(self, event=None):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000347 if sys.platform[:3] == 'win':
Steven M. Gava931625d2002-04-22 00:38:26 +0000348 os.startfile(self.help_url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000349 else:
350 webbrowser.open(self.help_url)
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000351 return "break"
David Scherer7aced172000-08-15 01:13:23 +0000352
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000353 def cut(self,event):
354 self.text.event_generate("<<Cut>>")
355 return "break"
356
357 def copy(self,event):
358 self.text.event_generate("<<Copy>>")
359 return "break"
360
361 def paste(self,event):
362 self.text.event_generate("<<Paste>>")
363 return "break"
364
David Scherer7aced172000-08-15 01:13:23 +0000365 def select_all(self, event=None):
366 self.text.tag_add("sel", "1.0", "end-1c")
367 self.text.mark_set("insert", "1.0")
368 self.text.see("insert")
369 return "break"
370
371 def remove_selection(self, event=None):
372 self.text.tag_remove("sel", "1.0", "end")
373 self.text.see("insert")
374
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000375 def move_at_edge_if_selection(self, edge_index):
376 """Cursor move begins at start or end of selection
377
378 When a left/right cursor key is pressed create and return to Tkinter a
379 function which causes a cursor move from the associated edge of the
380 selection.
381
382 """
383 self_text_index = self.text.index
384 self_text_mark_set = self.text.mark_set
385 edges_table = ("sel.first+1c", "sel.last-1c")
386 def move_at_edge(event):
387 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
388 try:
389 self_text_index("sel.first")
390 self_text_mark_set("insert", edges_table[edge_index])
391 except TclError:
392 pass
393 return move_at_edge
394
Kurt B. Kaiser3069dbb2005-01-28 00:16:16 +0000395 def del_word_left(self, event):
396 self.text.event_generate('<Meta-Delete>')
397 return "break"
398
399 def del_word_right(self, event):
400 self.text.event_generate('<Meta-d>')
401 return "break"
402
Steven M. Gavac5976402002-01-04 03:06:08 +0000403 def find_event(self, event):
404 SearchDialog.find(self.text)
405 return "break"
406
407 def find_again_event(self, event):
408 SearchDialog.find_again(self.text)
409 return "break"
410
411 def find_selection_event(self, event):
412 SearchDialog.find_selection(self.text)
413 return "break"
414
415 def find_in_files_event(self, event):
416 GrepDialog.grep(self.text, self.io, self.flist)
417 return "break"
418
419 def replace_event(self, event):
420 ReplaceDialog.replace(self.text)
421 return "break"
422
423 def goto_line_event(self, event):
424 text = self.text
425 lineno = tkSimpleDialog.askinteger("Goto",
426 "Go to line number:",parent=text)
427 if lineno is None:
428 return "break"
429 if lineno <= 0:
430 text.bell()
431 return "break"
432 text.mark_set("insert", "%d.0" % lineno)
433 text.see("insert")
434
David Scherer7aced172000-08-15 01:13:23 +0000435 def open_module(self, event=None):
436 # XXX Shouldn't this be in IOBinding or in FileList?
437 try:
438 name = self.text.get("sel.first", "sel.last")
439 except TclError:
440 name = ""
441 else:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000442 name = name.strip()
Guido van Rossum852f35b2003-06-05 11:36:55 +0000443 name = tkSimpleDialog.askstring("Module",
444 "Enter the name of a Python module\n"
445 "to search on sys.path and open:",
446 parent=self.text, initialvalue=name)
447 if name:
448 name = name.strip()
David Scherer7aced172000-08-15 01:13:23 +0000449 if not name:
Guido van Rossum852f35b2003-06-05 11:36:55 +0000450 return
David Scherer7aced172000-08-15 01:13:23 +0000451 # XXX Ought to insert current file's directory in front of path
452 try:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000453 (f, file, (suffix, mode, type)) = _find_module(name)
David Scherer7aced172000-08-15 01:13:23 +0000454 except (NameError, ImportError), msg:
455 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
456 return
457 if type != imp.PY_SOURCE:
458 tkMessageBox.showerror("Unsupported type",
459 "%s is not a source module" % name, parent=self.text)
460 return
461 if f:
462 f.close()
463 if self.flist:
464 self.flist.open(file)
465 else:
466 self.io.loadfile(file)
467
468 def open_class_browser(self, event=None):
469 filename = self.io.filename
470 if not filename:
471 tkMessageBox.showerror(
472 "No filename",
473 "This buffer has no associated filename",
474 master=self.text)
475 self.text.focus_set()
476 return None
477 head, tail = os.path.split(filename)
478 base, ext = os.path.splitext(tail)
479 import ClassBrowser
480 ClassBrowser.ClassBrowser(self.flist, base, [head])
481
482 def open_path_browser(self, event=None):
483 import PathBrowser
484 PathBrowser.PathBrowser(self.flist)
485
486 def gotoline(self, lineno):
487 if lineno is not None and lineno > 0:
488 self.text.mark_set("insert", "%d.0" % lineno)
489 self.text.tag_remove("sel", "1.0", "end")
490 self.text.tag_add("sel", "insert", "insert +1l")
491 self.center()
492
493 def ispythonsource(self, filename):
Kurt B. Kaiserdf506ea2005-06-12 04:33:30 +0000494 if not filename or os.path.isdir(filename):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000495 return True
David Scherer7aced172000-08-15 01:13:23 +0000496 base, ext = os.path.splitext(os.path.basename(filename))
497 if os.path.normcase(ext) in (".py", ".pyw"):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000498 return True
David Scherer7aced172000-08-15 01:13:23 +0000499 try:
500 f = open(filename)
501 line = f.readline()
502 f.close()
503 except IOError:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000504 return False
Kurt B. Kaiser83a35602002-12-20 17:18:03 +0000505 return line.startswith('#!') and line.find('python') >= 0
David Scherer7aced172000-08-15 01:13:23 +0000506
507 def close_hook(self):
508 if self.flist:
509 self.flist.close_edit(self)
510
511 def set_close_hook(self, close_hook):
512 self.close_hook = close_hook
513
514 def filename_change_hook(self):
515 if self.flist:
516 self.flist.filename_changed_edit(self)
517 self.saved_change_hook()
Kurt B. Kaiser260cb902003-06-06 21:58:38 +0000518 self.top.update_windowlist_registry(self)
David Scherer7aced172000-08-15 01:13:23 +0000519 if self.ispythonsource(self.io.filename):
520 self.addcolorizer()
521 else:
522 self.rmcolorizer()
523
524 def addcolorizer(self):
525 if self.color:
526 return
David Scherer7aced172000-08-15 01:13:23 +0000527 self.per.removefilter(self.undo)
528 self.color = self.ColorDelegator()
529 self.per.insertfilter(self.color)
530 self.per.insertfilter(self.undo)
531
532 def rmcolorizer(self):
533 if not self.color:
534 return
Kurt B. Kaiserdf506ea2005-06-12 04:33:30 +0000535 self.color.removecolors()
David Scherer7aced172000-08-15 01:13:23 +0000536 self.per.removefilter(self.undo)
537 self.per.removefilter(self.color)
538 self.color = None
539 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000540
Steven M. Gavab77d3432002-03-02 07:16:21 +0000541 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000542 "Update the colour theme if it is changed"
543 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000544 if self.color:
545 self.color = self.ColorDelegator()
546 self.per.insertfilter(self.color)
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000547 theme = idleConf.GetOption('main','Theme','name')
548 self.text.config(idleConf.GetHighlight(theme, "normal"))
David Scherer7aced172000-08-15 01:13:23 +0000549
Steven M. Gavab1585412002-03-12 00:21:56 +0000550 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000551 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000552 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000553 fontWeight='normal'
554 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
555 fontWeight='bold'
556 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
557 idleConf.GetOption('main','EditorWindow','font-size'),
558 fontWeight))
559
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000560 def ResetKeybindings(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000561 "Update the keybindings if they are changed"
562 # Called from configDialog.py
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000563 self.Bindings.default_keydefs=idleConf.GetCurrentKeySet()
564 keydefs = self.Bindings.default_keydefs
565 for event, keylist in keydefs.items():
566 self.text.event_delete(event)
567 self.apply_bindings()
568 #update menu accelerators
569 menuEventDict={}
570 for menu in self.Bindings.menudefs:
571 menuEventDict[menu[0]]={}
572 for item in menu[1]:
573 if item:
574 menuEventDict[menu[0]][prepstr(item[0])[1]]=item[1]
575 for menubarItem in self.menudict.keys():
576 menu=self.menudict[menubarItem]
577 end=menu.index(END)+1
578 for index in range(0,end):
579 if menu.type(index)=='command':
580 accel=menu.entrycget(index,'accelerator')
581 if accel:
582 itemName=menu.entrycget(index,'label')
583 event=''
584 if menuEventDict.has_key(menubarItem):
585 if menuEventDict[menubarItem].has_key(itemName):
586 event=menuEventDict[menubarItem][itemName]
587 if event:
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000588 accel=get_accelerator(keydefs, event)
589 menu.entryconfig(index,accelerator=accel)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000590
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000591 def set_notabs_indentwidth(self):
592 "Update the indentwidth if changed and not using tabs in this window"
593 # Called from configDialog.py
594 if not self.usetabs:
595 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
596 type='int')
597
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000598 def reset_help_menu_entries(self):
599 "Update the additional help entries on the Help menu"
600 help_list = idleConf.GetAllExtraHelpSourcesList()
601 helpmenu = self.menudict['help']
602 # first delete the extra help entries, if any
603 helpmenu_length = helpmenu.index(END)
604 if helpmenu_length > self.base_helpmenu_length:
605 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
606 # then rebuild them
607 if help_list:
608 helpmenu.add_separator()
609 for entry in help_list:
610 cmd = self.__extra_help_callback(entry[1])
611 helpmenu.add_command(label=entry[0], command=cmd)
612 # and update the menu dictionary
613 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000614
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000615 def __extra_help_callback(self, helpfile):
616 "Create a callback with the helpfile value frozen at definition time"
617 def display_extra_help(helpfile=helpfile):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000618 if not (helpfile.startswith('www') or helpfile.startswith('http')):
619 url = os.path.normpath(helpfile)
620 if sys.platform[:3] == 'win':
621 os.startfile(helpfile)
622 else:
623 webbrowser.open(helpfile)
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000624 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000625
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000626 def update_recent_files_list(self, new_file=None):
627 "Load and update the recent files list and menus"
628 rf_list = []
629 if os.path.exists(self.recent_files_path):
630 rf_list_file = open(self.recent_files_path,'r')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000631 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000632 rf_list = rf_list_file.readlines()
Steven M. Gava1d46e402002-03-27 08:40:46 +0000633 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000634 rf_list_file.close()
635 if new_file:
636 new_file = os.path.abspath(new_file) + '\n'
637 if new_file in rf_list:
638 rf_list.remove(new_file) # move to top
639 rf_list.insert(0, new_file)
640 # clean and save the recent files list
641 bad_paths = []
642 for path in rf_list:
643 if '\0' in path or not os.path.exists(path[0:-1]):
644 bad_paths.append(path)
645 rf_list = [path for path in rf_list if path not in bad_paths]
646 ulchars = "1234567890ABCDEFGHIJK"
647 rf_list = rf_list[0:len(ulchars)]
648 rf_file = open(self.recent_files_path, 'w')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000649 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000650 rf_file.writelines(rf_list)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000651 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000652 rf_file.close()
653 # for each edit window instance, construct the recent files menu
654 for instance in self.top.instance_dict.keys():
655 menu = instance.recent_files_menu
656 menu.delete(1, END) # clear, and rebuild:
657 for i, file in zip(count(), rf_list):
658 file_name = file[0:-1] # zap \n
659 callback = instance.__recent_file_callback(file_name)
660 menu.add_command(label=ulchars[i] + " " + file_name,
661 command=callback,
662 underline=0)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000663
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000664 def __recent_file_callback(self, file_name):
665 def open_recent_file(fn_closure=file_name):
666 self.io.open(editFile=fn_closure)
667 return open_recent_file
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000668
David Scherer7aced172000-08-15 01:13:23 +0000669 def saved_change_hook(self):
670 short = self.short_title()
671 long = self.long_title()
672 if short and long:
673 title = short + " - " + long
674 elif short:
675 title = short
676 elif long:
677 title = long
678 else:
679 title = "Untitled"
680 icon = short or long or title
681 if not self.get_saved():
682 title = "*%s*" % title
683 icon = "*%s" % icon
684 self.top.wm_title(title)
685 self.top.wm_iconname(icon)
686
687 def get_saved(self):
688 return self.undo.get_saved()
689
690 def set_saved(self, flag):
691 self.undo.set_saved(flag)
692
693 def reset_undo(self):
694 self.undo.reset_undo()
695
696 def short_title(self):
697 filename = self.io.filename
698 if filename:
699 filename = os.path.basename(filename)
700 return filename
701
702 def long_title(self):
703 return self.io.filename or ""
704
705 def center_insert_event(self, event):
706 self.center()
707
708 def center(self, mark="insert"):
709 text = self.text
710 top, bot = self.getwindowlines()
711 lineno = self.getlineno(mark)
712 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000713 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000714 text.yview(float(newtop))
715
716 def getwindowlines(self):
717 text = self.text
718 top = self.getlineno("@0,0")
719 bot = self.getlineno("@0,65535")
720 if top == bot and text.winfo_height() == 1:
721 # Geometry manager hasn't run yet
722 height = int(text['height'])
723 bot = top + height - 1
724 return top, bot
725
726 def getlineno(self, mark="insert"):
727 text = self.text
728 return int(float(text.index(mark)))
729
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000730 def get_geometry(self):
731 "Return (width, height, x, y)"
732 geom = self.top.wm_geometry()
733 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
734 tuple = (map(int, m.groups()))
735 return tuple
736
David Scherer7aced172000-08-15 01:13:23 +0000737 def close_event(self, event):
738 self.close()
739
740 def maybesave(self):
741 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000742 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000743 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000744 self.top.deiconify()
745 self.top.lower()
746 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000747 return self.io.maybesave()
748
749 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000750 reply = self.maybesave()
751 if reply != "cancel":
752 self._close()
753 return reply
754
755 def _close(self):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000756 if self.io.filename:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000757 self.update_recent_files_list(new_file=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000758 WindowList.unregister_callback(self.postwindowsmenu)
759 if self.close_hook:
760 self.close_hook()
761 self.flist = None
762 colorizing = 0
763 self.unload_extensions()
764 self.io.close(); self.io = None
765 self.undo = None # XXX
766 if self.color:
767 colorizing = self.color.colorizing
768 doh = colorizing and self.top
769 self.color.close(doh) # Cancel colorization
770 self.text = None
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000771 self.tkinter_vars = None
David Scherer7aced172000-08-15 01:13:23 +0000772 self.per.close(); self.per = None
773 if not colorizing:
774 self.top.destroy()
775
776 def load_extensions(self):
777 self.extensions = {}
778 self.load_standard_extensions()
779
780 def unload_extensions(self):
781 for ins in self.extensions.values():
782 if hasattr(ins, "close"):
783 ins.close()
784 self.extensions = {}
785
786 def load_standard_extensions(self):
787 for name in self.get_standard_extension_names():
788 try:
789 self.load_extension(name)
790 except:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000791 print "Failed to load extension", repr(name)
David Scherer7aced172000-08-15 01:13:23 +0000792 import traceback
793 traceback.print_exc()
794
795 def get_standard_extension_names(self):
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000796 return idleConf.GetExtensions(editor_only=True)
David Scherer7aced172000-08-15 01:13:23 +0000797
798 def load_extension(self, name):
Kurt B. Kaiserb00e89f2005-01-18 00:54:58 +0000799 try:
800 mod = __import__(name, globals(), locals(), [])
801 except ImportError:
802 print "\nFailed to import extension: ", name
803 return
David Scherer7aced172000-08-15 01:13:23 +0000804 cls = getattr(mod, name)
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000805 keydefs = idleConf.GetExtensionBindings(name)
806 if hasattr(cls, "menudefs"):
807 self.fill_menus(cls.menudefs, keydefs)
David Scherer7aced172000-08-15 01:13:23 +0000808 ins = cls(self)
809 self.extensions[name] = ins
David Scherer7aced172000-08-15 01:13:23 +0000810 if keydefs:
811 self.apply_bindings(keydefs)
812 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000813 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000814 while methodname[:1] == '<':
815 methodname = methodname[1:]
816 while methodname[-1:] == '>':
817 methodname = methodname[:-1]
818 methodname = methodname + "_event"
819 if hasattr(ins, methodname):
820 self.text.bind(vevent, getattr(ins, methodname))
David Scherer7aced172000-08-15 01:13:23 +0000821
822 def apply_bindings(self, keydefs=None):
823 if keydefs is None:
824 keydefs = self.Bindings.default_keydefs
825 text = self.text
826 text.keydefs = keydefs
827 for event, keylist in keydefs.items():
828 if keylist:
Raymond Hettinger931237e2003-07-09 18:48:24 +0000829 text.event_add(event, *keylist)
David Scherer7aced172000-08-15 01:13:23 +0000830
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000831 def fill_menus(self, menudefs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000832 """Add appropriate entries to the menus and submenus
833
834 Menus that are absent or None in self.menudict are ignored.
835 """
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000836 if menudefs is None:
837 menudefs = self.Bindings.menudefs
David Scherer7aced172000-08-15 01:13:23 +0000838 if keydefs is None:
839 keydefs = self.Bindings.default_keydefs
840 menudict = self.menudict
841 text = self.text
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000842 for mname, entrylist in menudefs:
David Scherer7aced172000-08-15 01:13:23 +0000843 menu = menudict.get(mname)
844 if not menu:
845 continue
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000846 for entry in entrylist:
847 if not entry:
David Scherer7aced172000-08-15 01:13:23 +0000848 menu.add_separator()
849 else:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000850 label, eventname = entry
David Scherer7aced172000-08-15 01:13:23 +0000851 checkbutton = (label[:1] == '!')
852 if checkbutton:
853 label = label[1:]
854 underline, label = prepstr(label)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000855 accelerator = get_accelerator(keydefs, eventname)
856 def command(text=text, eventname=eventname):
857 text.event_generate(eventname)
David Scherer7aced172000-08-15 01:13:23 +0000858 if checkbutton:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000859 var = self.get_var_obj(eventname, BooleanVar)
David Scherer7aced172000-08-15 01:13:23 +0000860 menu.add_checkbutton(label=label, underline=underline,
861 command=command, accelerator=accelerator,
862 variable=var)
863 else:
864 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000865 command=command,
866 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000867
868 def getvar(self, name):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000869 var = self.get_var_obj(name)
David Scherer7aced172000-08-15 01:13:23 +0000870 if var:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000871 value = var.get()
872 return value
873 else:
874 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000875
876 def setvar(self, name, value, vartype=None):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000877 var = self.get_var_obj(name, vartype)
David Scherer7aced172000-08-15 01:13:23 +0000878 if var:
879 var.set(value)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000880 else:
881 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000882
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000883 def get_var_obj(self, name, vartype=None):
884 var = self.tkinter_vars.get(name)
David Scherer7aced172000-08-15 01:13:23 +0000885 if not var and vartype:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000886 # create a Tkinter variable object with self.text as master:
887 self.tkinter_vars[name] = var = vartype(self.text)
David Scherer7aced172000-08-15 01:13:23 +0000888 return var
889
890 # Tk implementations of "virtual text methods" -- each platform
891 # reusing IDLE's support code needs to define these for its GUI's
892 # flavor of widget.
893
894 # Is character at text_index in a Python string? Return 0 for
895 # "guaranteed no", true for anything else. This info is expensive
896 # to compute ab initio, but is probably already known by the
897 # platform's colorizer.
898
899 def is_char_in_string(self, text_index):
900 if self.color:
901 # Return true iff colorizer hasn't (re)gotten this far
902 # yet, or the character is tagged as being in a string
903 return self.text.tag_prevrange("TODO", text_index) or \
904 "STRING" in self.text.tag_names(text_index)
905 else:
906 # The colorizer is missing: assume the worst
907 return 1
908
909 # If a selection is defined in the text widget, return (start,
910 # end) as Tkinter text indices, otherwise return (None, None)
911 def get_selection_indices(self):
912 try:
913 first = self.text.index("sel.first")
914 last = self.text.index("sel.last")
915 return first, last
916 except TclError:
917 return None, None
918
919 # Return the text widget's current view of what a tab stop means
920 # (equivalent width in spaces).
921
922 def get_tabwidth(self):
923 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
924 return int(current)
925
926 # Set the text widget's current view of what a tab stop means.
927
928 def set_tabwidth(self, newtabwidth):
929 text = self.text
930 if self.get_tabwidth() != newtabwidth:
931 pixels = text.tk.call("font", "measure", text["font"],
932 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000933 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000934 text.configure(tabs=pixels)
935
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000936 # If ispythonsource and guess are true, guess a good value for
937 # indentwidth based on file content (if possible), and if
938 # indentwidth != tabwidth set usetabs false.
939 # In any case, adjust the Text widget's view of what a tab
940 # character means.
941
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000942 def set_indentation_params(self, ispythonsource, guess=True):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000943 if guess and ispythonsource:
944 i = self.guess_indent()
945 if 2 <= i <= 8:
946 self.indentwidth = i
947 if self.indentwidth != self.tabwidth:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000948 self.usetabs = False
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000949 self.set_tabwidth(self.tabwidth)
950
951 def smart_backspace_event(self, event):
952 text = self.text
953 first, last = self.get_selection_indices()
954 if first and last:
955 text.delete(first, last)
956 text.mark_set("insert", first)
957 return "break"
958 # Delete whitespace left, until hitting a real char or closest
959 # preceding virtual tab stop.
960 chars = text.get("insert linestart", "insert")
961 if chars == '':
962 if text.compare("insert", ">", "1.0"):
963 # easy: delete preceding newline
964 text.delete("insert-1c")
965 else:
966 text.bell() # at start of buffer
967 return "break"
968 if chars[-1] not in " \t":
969 # easy: delete preceding real char
970 text.delete("insert-1c")
971 return "break"
972 # Ick. It may require *inserting* spaces if we back up over a
973 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000974 tabwidth = self.tabwidth
975 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000976 assert have > 0
977 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000978 # Debug prompt is multilined....
979 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000980 ncharsdeleted = 0
981 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000982 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +0000983 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000984 chars = chars[:-1]
985 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000986 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000987 if have <= want or chars[-1] not in " \t":
988 break
989 text.undo_block_start()
990 text.delete("insert-%dc" % ncharsdeleted, "insert")
991 if have < want:
992 text.insert("insert", ' ' * (want - have))
993 text.undo_block_stop()
994 return "break"
995
996 def smart_indent_event(self, event):
997 # if intraline selection:
998 # delete it
999 # elif multiline selection:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001000 # do indent-region
1001 # else:
1002 # indent one level
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001003 text = self.text
1004 first, last = self.get_selection_indices()
1005 text.undo_block_start()
1006 try:
1007 if first and last:
1008 if index2line(first) != index2line(last):
1009 return self.indent_region_event(event)
1010 text.delete(first, last)
1011 text.mark_set("insert", first)
1012 prefix = text.get("insert linestart", "insert")
1013 raw, effective = classifyws(prefix, self.tabwidth)
1014 if raw == len(prefix):
1015 # only whitespace to the left
1016 self.reindent_to(effective + self.indentwidth)
1017 else:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001018 # tab to the next 'stop' within or to right of line's text:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001019 if self.usetabs:
1020 pad = '\t'
1021 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001022 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001023 n = self.indentwidth
1024 pad = ' ' * (n - effective % n)
1025 text.insert("insert", pad)
1026 text.see("insert")
1027 return "break"
1028 finally:
1029 text.undo_block_stop()
1030
1031 def newline_and_indent_event(self, event):
1032 text = self.text
1033 first, last = self.get_selection_indices()
1034 text.undo_block_start()
1035 try:
1036 if first and last:
1037 text.delete(first, last)
1038 text.mark_set("insert", first)
1039 line = text.get("insert linestart", "insert")
1040 i, n = 0, len(line)
1041 while i < n and line[i] in " \t":
1042 i = i+1
1043 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001044 # the cursor is in or at leading indentation in a continuation
1045 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001046 text.insert("insert linestart", '\n')
1047 return "break"
1048 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001049 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001050 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001051 last_line_of_prompt = sys.ps1.split('\n')[-1]
1052 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001053 line = line[:-1]
1054 i = i+1
1055 if i:
1056 text.delete("insert - %d chars" % i, "insert")
1057 # strip whitespace after insert point
1058 while text.get("insert") in " \t":
1059 text.delete("insert")
1060 # start new line
1061 text.insert("insert", '\n')
1062
1063 # adjust indentation for continuations and block
1064 # open/close first need to find the last stmt
1065 lno = index2line(text.index('insert'))
1066 y = PyParse.Parser(self.indentwidth, self.tabwidth)
1067 for context in self.num_context_lines:
1068 startat = max(lno - context, 1)
Walter Dörwald70a6b492004-02-12 17:35:32 +00001069 startatindex = repr(startat) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001070 rawtext = text.get(startatindex, "insert")
1071 y.set_str(rawtext)
1072 bod = y.find_good_parse_start(
1073 self.context_use_ps1,
1074 self._build_char_in_string_func(startatindex))
1075 if bod is not None or startat == 1:
1076 break
1077 y.set_lo(bod or 0)
1078 c = y.get_continuation_type()
1079 if c != PyParse.C_NONE:
1080 # The current stmt hasn't ended yet.
Kurt B. Kaiserb61602c2005-11-15 07:20:06 +00001081 if c == PyParse.C_STRING_FIRST_LINE:
1082 # after the first line of a string; do not indent at all
1083 pass
1084 elif c == PyParse.C_STRING_NEXT_LINES:
1085 # inside a string which started before this line;
1086 # just mimic the current indent
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001087 text.insert("insert", indent)
1088 elif c == PyParse.C_BRACKET:
1089 # line up with the first (if any) element of the
1090 # last open bracket structure; else indent one
1091 # level beyond the indent of the line with the
1092 # last open bracket
1093 self.reindent_to(y.compute_bracket_indent())
1094 elif c == PyParse.C_BACKSLASH:
1095 # if more than one line in this stmt already, just
1096 # mimic the current indent; else if initial line
1097 # has a start on an assignment stmt, indent to
1098 # beyond leftmost =; else to beyond first chunk of
1099 # non-whitespace on initial line
1100 if y.get_num_lines_in_stmt() > 1:
1101 text.insert("insert", indent)
1102 else:
1103 self.reindent_to(y.compute_backslash_indent())
1104 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001105 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001106 return "break"
1107
1108 # This line starts a brand new stmt; indent relative to
1109 # indentation of initial line of closest preceding
1110 # interesting stmt.
1111 indent = y.get_base_indent_string()
1112 text.insert("insert", indent)
1113 if y.is_block_opener():
1114 self.smart_indent_event(event)
1115 elif indent and y.is_block_closer():
1116 self.smart_backspace_event(event)
1117 return "break"
1118 finally:
1119 text.see("insert")
1120 text.undo_block_stop()
1121
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001122 # Our editwin provides a is_char_in_string function that works
1123 # with a Tk text index, but PyParse only knows about offsets into
1124 # a string. This builds a function for PyParse that accepts an
1125 # offset.
1126
1127 def _build_char_in_string_func(self, startindex):
1128 def inner(offset, _startindex=startindex,
1129 _icis=self.is_char_in_string):
1130 return _icis(_startindex + "+%dc" % offset)
1131 return inner
1132
1133 def indent_region_event(self, event):
1134 head, tail, chars, lines = self.get_region()
1135 for pos in range(len(lines)):
1136 line = lines[pos]
1137 if line:
1138 raw, effective = classifyws(line, self.tabwidth)
1139 effective = effective + self.indentwidth
1140 lines[pos] = self._make_blanks(effective) + line[raw:]
1141 self.set_region(head, tail, chars, lines)
1142 return "break"
1143
1144 def dedent_region_event(self, event):
1145 head, tail, chars, lines = self.get_region()
1146 for pos in range(len(lines)):
1147 line = lines[pos]
1148 if line:
1149 raw, effective = classifyws(line, self.tabwidth)
1150 effective = max(effective - self.indentwidth, 0)
1151 lines[pos] = self._make_blanks(effective) + line[raw:]
1152 self.set_region(head, tail, chars, lines)
1153 return "break"
1154
1155 def comment_region_event(self, event):
1156 head, tail, chars, lines = self.get_region()
1157 for pos in range(len(lines) - 1):
1158 line = lines[pos]
1159 lines[pos] = '##' + line
1160 self.set_region(head, tail, chars, lines)
1161
1162 def uncomment_region_event(self, event):
1163 head, tail, chars, lines = self.get_region()
1164 for pos in range(len(lines)):
1165 line = lines[pos]
1166 if not line:
1167 continue
1168 if line[:2] == '##':
1169 line = line[2:]
1170 elif line[:1] == '#':
1171 line = line[1:]
1172 lines[pos] = line
1173 self.set_region(head, tail, chars, lines)
1174
1175 def tabify_region_event(self, event):
1176 head, tail, chars, lines = self.get_region()
1177 tabwidth = self._asktabwidth()
1178 for pos in range(len(lines)):
1179 line = lines[pos]
1180 if line:
1181 raw, effective = classifyws(line, tabwidth)
1182 ntabs, nspaces = divmod(effective, tabwidth)
1183 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1184 self.set_region(head, tail, chars, lines)
1185
1186 def untabify_region_event(self, event):
1187 head, tail, chars, lines = self.get_region()
1188 tabwidth = self._asktabwidth()
1189 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001190 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001191 self.set_region(head, tail, chars, lines)
1192
1193 def toggle_tabs_event(self, event):
1194 if self.askyesno(
1195 "Toggle tabs",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001196 "Turn tabs " + ("on", "off")[self.usetabs] +
1197 "?\nIndent width " +
1198 ("will be", "remains at")[self.usetabs] + " 8.",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001199 parent=self.text):
1200 self.usetabs = not self.usetabs
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001201 # Try to prevent mixed tabs/spaces.
1202 # User must reset indent width manually after using tabs
1203 # if he insists on getting into trouble.
1204 self.indentwidth = 8
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001205 return "break"
1206
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001207 # XXX this isn't bound to anything -- see tabwidth comments
1208## def change_tabwidth_event(self, event):
1209## new = self._asktabwidth()
1210## if new != self.tabwidth:
1211## self.tabwidth = new
1212## self.set_indentation_params(0, guess=0)
1213## return "break"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001214
1215 def change_indentwidth_event(self, event):
1216 new = self.askinteger(
1217 "Indent width",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001218 "New indent width (2-16)\n(Always use 8 when using tabs)",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001219 parent=self.text,
1220 initialvalue=self.indentwidth,
1221 minvalue=2,
1222 maxvalue=16)
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001223 if new and new != self.indentwidth and not self.usetabs:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001224 self.indentwidth = new
1225 return "break"
1226
1227 def get_region(self):
1228 text = self.text
1229 first, last = self.get_selection_indices()
1230 if first and last:
1231 head = text.index(first + " linestart")
1232 tail = text.index(last + "-1c lineend +1c")
1233 else:
1234 head = text.index("insert linestart")
1235 tail = text.index("insert lineend +1c")
1236 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001237 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001238 return head, tail, chars, lines
1239
1240 def set_region(self, head, tail, chars, lines):
1241 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001242 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001243 if newchars == chars:
1244 text.bell()
1245 return
1246 text.tag_remove("sel", "1.0", "end")
1247 text.mark_set("insert", head)
1248 text.undo_block_start()
1249 text.delete(head, tail)
1250 text.insert(head, newchars)
1251 text.undo_block_stop()
1252 text.tag_add("sel", head, "insert")
1253
1254 # Make string that displays as n leading blanks.
1255
1256 def _make_blanks(self, n):
1257 if self.usetabs:
1258 ntabs, nspaces = divmod(n, self.tabwidth)
1259 return '\t' * ntabs + ' ' * nspaces
1260 else:
1261 return ' ' * n
1262
1263 # Delete from beginning of line to insert point, then reinsert
1264 # column logical (meaning use tabs if appropriate) spaces.
1265
1266 def reindent_to(self, column):
1267 text = self.text
1268 text.undo_block_start()
1269 if text.compare("insert linestart", "!=", "insert"):
1270 text.delete("insert linestart", "insert")
1271 if column:
1272 text.insert("insert", self._make_blanks(column))
1273 text.undo_block_stop()
1274
1275 def _asktabwidth(self):
1276 return self.askinteger(
1277 "Tab width",
Kurt B. Kaiserca7329c2005-06-12 05:19:23 +00001278 "Columns per tab? (2-16)",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001279 parent=self.text,
1280 initialvalue=self.indentwidth,
1281 minvalue=2,
1282 maxvalue=16) or self.tabwidth
1283
1284 # Guess indentwidth from text content.
1285 # Return guessed indentwidth. This should not be believed unless
1286 # it's in a reasonable range (e.g., it will be 0 if no indented
1287 # blocks are found).
1288
1289 def guess_indent(self):
1290 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1291 if opener and indented:
1292 raw, indentsmall = classifyws(opener, self.tabwidth)
1293 raw, indentlarge = classifyws(indented, self.tabwidth)
1294 else:
1295 indentsmall = indentlarge = 0
1296 return indentlarge - indentsmall
1297
1298# "line.col" -> line, as an int
1299def index2line(index):
1300 return int(float(index))
1301
1302# Look at the leading whitespace in s.
1303# Return pair (# of leading ws characters,
1304# effective # of leading blanks after expanding
1305# tabs to width tabwidth)
1306
1307def classifyws(s, tabwidth):
1308 raw = effective = 0
1309 for ch in s:
1310 if ch == ' ':
1311 raw = raw + 1
1312 effective = effective + 1
1313 elif ch == '\t':
1314 raw = raw + 1
1315 effective = (effective // tabwidth + 1) * tabwidth
1316 else:
1317 break
1318 return raw, effective
1319
1320import tokenize
1321_tokenize = tokenize
1322del tokenize
1323
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +00001324class IndentSearcher(object):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001325
1326 # .run() chews over the Text widget, looking for a block opener
1327 # and the stmt following it. Returns a pair,
1328 # (line containing block opener, line containing stmt)
1329 # Either or both may be None.
1330
1331 def __init__(self, text, tabwidth):
1332 self.text = text
1333 self.tabwidth = tabwidth
1334 self.i = self.finished = 0
1335 self.blkopenline = self.indentedline = None
1336
1337 def readline(self):
1338 if self.finished:
1339 return ""
1340 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001341 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001342 if self.text.compare(mark, ">=", "end"):
1343 return ""
1344 return self.text.get(mark, mark + " lineend+1c")
1345
1346 def tokeneater(self, type, token, start, end, line,
1347 INDENT=_tokenize.INDENT,
1348 NAME=_tokenize.NAME,
1349 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1350 if self.finished:
1351 pass
1352 elif type == NAME and token in OPENERS:
1353 self.blkopenline = line
1354 elif type == INDENT and self.blkopenline:
1355 self.indentedline = line
1356 self.finished = 1
1357
1358 def run(self):
1359 save_tabsize = _tokenize.tabsize
1360 _tokenize.tabsize = self.tabwidth
1361 try:
1362 try:
1363 _tokenize.tokenize(self.readline, self.tokeneater)
1364 except _tokenize.TokenError:
1365 # since we cut off the tokenizer early, we can trigger
1366 # spurious errors
1367 pass
1368 finally:
1369 _tokenize.tabsize = save_tabsize
1370 return self.blkopenline, self.indentedline
1371
1372### end autoindent code ###
1373
David Scherer7aced172000-08-15 01:13:23 +00001374def prepstr(s):
1375 # Helper to extract the underscore from a string, e.g.
1376 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001377 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001378 if i >= 0:
1379 s = s[:i] + s[i+1:]
1380 return i, s
1381
1382
1383keynames = {
1384 'bracketleft': '[',
1385 'bracketright': ']',
1386 'slash': '/',
1387}
1388
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001389def get_accelerator(keydefs, eventname):
1390 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001391 if not keylist:
1392 return ""
1393 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001394 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001395 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1396 s = re.sub("Key-", "", s)
1397 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1398 s = re.sub("Control-", "Ctrl-", s)
1399 s = re.sub("-", "+", s)
1400 s = re.sub("><", " ", s)
1401 s = re.sub("<", "", s)
1402 s = re.sub(">", "", s)
1403 return s
1404
1405
1406def fixwordbreaks(root):
1407 # Make sure that Tk's double-click and next/previous word
1408 # operations use our definition of a word (i.e. an identifier)
1409 tk = root.tk
1410 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1411 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1412 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1413
1414
1415def test():
1416 root = Tk()
1417 fixwordbreaks(root)
1418 root.withdraw()
1419 if sys.argv[1:]:
1420 filename = sys.argv[1]
1421 else:
1422 filename = None
1423 edit = EditorWindow(root=root, filename=filename)
1424 edit.set_close_hook(root.quit)
1425 root.mainloop()
1426 root.destroy()
1427
1428if __name__ == '__main__':
1429 test()