blob: ef825f18895b6d505aa69fc96924c8060368eecd [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.
1081 if c == PyParse.C_STRING:
1082 # inside a string; just mimic the current indent
1083 text.insert("insert", indent)
1084 elif c == PyParse.C_BRACKET:
1085 # line up with the first (if any) element of the
1086 # last open bracket structure; else indent one
1087 # level beyond the indent of the line with the
1088 # last open bracket
1089 self.reindent_to(y.compute_bracket_indent())
1090 elif c == PyParse.C_BACKSLASH:
1091 # if more than one line in this stmt already, just
1092 # mimic the current indent; else if initial line
1093 # has a start on an assignment stmt, indent to
1094 # beyond leftmost =; else to beyond first chunk of
1095 # non-whitespace on initial line
1096 if y.get_num_lines_in_stmt() > 1:
1097 text.insert("insert", indent)
1098 else:
1099 self.reindent_to(y.compute_backslash_indent())
1100 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001101 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001102 return "break"
1103
1104 # This line starts a brand new stmt; indent relative to
1105 # indentation of initial line of closest preceding
1106 # interesting stmt.
1107 indent = y.get_base_indent_string()
1108 text.insert("insert", indent)
1109 if y.is_block_opener():
1110 self.smart_indent_event(event)
1111 elif indent and y.is_block_closer():
1112 self.smart_backspace_event(event)
1113 return "break"
1114 finally:
1115 text.see("insert")
1116 text.undo_block_stop()
1117
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001118 # Our editwin provides a is_char_in_string function that works
1119 # with a Tk text index, but PyParse only knows about offsets into
1120 # a string. This builds a function for PyParse that accepts an
1121 # offset.
1122
1123 def _build_char_in_string_func(self, startindex):
1124 def inner(offset, _startindex=startindex,
1125 _icis=self.is_char_in_string):
1126 return _icis(_startindex + "+%dc" % offset)
1127 return inner
1128
1129 def indent_region_event(self, event):
1130 head, tail, chars, lines = self.get_region()
1131 for pos in range(len(lines)):
1132 line = lines[pos]
1133 if line:
1134 raw, effective = classifyws(line, self.tabwidth)
1135 effective = effective + self.indentwidth
1136 lines[pos] = self._make_blanks(effective) + line[raw:]
1137 self.set_region(head, tail, chars, lines)
1138 return "break"
1139
1140 def dedent_region_event(self, event):
1141 head, tail, chars, lines = self.get_region()
1142 for pos in range(len(lines)):
1143 line = lines[pos]
1144 if line:
1145 raw, effective = classifyws(line, self.tabwidth)
1146 effective = max(effective - self.indentwidth, 0)
1147 lines[pos] = self._make_blanks(effective) + line[raw:]
1148 self.set_region(head, tail, chars, lines)
1149 return "break"
1150
1151 def comment_region_event(self, event):
1152 head, tail, chars, lines = self.get_region()
1153 for pos in range(len(lines) - 1):
1154 line = lines[pos]
1155 lines[pos] = '##' + line
1156 self.set_region(head, tail, chars, lines)
1157
1158 def uncomment_region_event(self, event):
1159 head, tail, chars, lines = self.get_region()
1160 for pos in range(len(lines)):
1161 line = lines[pos]
1162 if not line:
1163 continue
1164 if line[:2] == '##':
1165 line = line[2:]
1166 elif line[:1] == '#':
1167 line = line[1:]
1168 lines[pos] = line
1169 self.set_region(head, tail, chars, lines)
1170
1171 def tabify_region_event(self, event):
1172 head, tail, chars, lines = self.get_region()
1173 tabwidth = self._asktabwidth()
1174 for pos in range(len(lines)):
1175 line = lines[pos]
1176 if line:
1177 raw, effective = classifyws(line, tabwidth)
1178 ntabs, nspaces = divmod(effective, tabwidth)
1179 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1180 self.set_region(head, tail, chars, lines)
1181
1182 def untabify_region_event(self, event):
1183 head, tail, chars, lines = self.get_region()
1184 tabwidth = self._asktabwidth()
1185 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001186 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001187 self.set_region(head, tail, chars, lines)
1188
1189 def toggle_tabs_event(self, event):
1190 if self.askyesno(
1191 "Toggle tabs",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001192 "Turn tabs " + ("on", "off")[self.usetabs] +
1193 "?\nIndent width " +
1194 ("will be", "remains at")[self.usetabs] + " 8.",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001195 parent=self.text):
1196 self.usetabs = not self.usetabs
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001197 # Try to prevent mixed tabs/spaces.
1198 # User must reset indent width manually after using tabs
1199 # if he insists on getting into trouble.
1200 self.indentwidth = 8
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001201 return "break"
1202
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001203 # XXX this isn't bound to anything -- see tabwidth comments
1204## def change_tabwidth_event(self, event):
1205## new = self._asktabwidth()
1206## if new != self.tabwidth:
1207## self.tabwidth = new
1208## self.set_indentation_params(0, guess=0)
1209## return "break"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001210
1211 def change_indentwidth_event(self, event):
1212 new = self.askinteger(
1213 "Indent width",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001214 "New indent width (2-16)\n(Always use 8 when using tabs)",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001215 parent=self.text,
1216 initialvalue=self.indentwidth,
1217 minvalue=2,
1218 maxvalue=16)
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001219 if new and new != self.indentwidth and not self.usetabs:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001220 self.indentwidth = new
1221 return "break"
1222
1223 def get_region(self):
1224 text = self.text
1225 first, last = self.get_selection_indices()
1226 if first and last:
1227 head = text.index(first + " linestart")
1228 tail = text.index(last + "-1c lineend +1c")
1229 else:
1230 head = text.index("insert linestart")
1231 tail = text.index("insert lineend +1c")
1232 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001233 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001234 return head, tail, chars, lines
1235
1236 def set_region(self, head, tail, chars, lines):
1237 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001238 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001239 if newchars == chars:
1240 text.bell()
1241 return
1242 text.tag_remove("sel", "1.0", "end")
1243 text.mark_set("insert", head)
1244 text.undo_block_start()
1245 text.delete(head, tail)
1246 text.insert(head, newchars)
1247 text.undo_block_stop()
1248 text.tag_add("sel", head, "insert")
1249
1250 # Make string that displays as n leading blanks.
1251
1252 def _make_blanks(self, n):
1253 if self.usetabs:
1254 ntabs, nspaces = divmod(n, self.tabwidth)
1255 return '\t' * ntabs + ' ' * nspaces
1256 else:
1257 return ' ' * n
1258
1259 # Delete from beginning of line to insert point, then reinsert
1260 # column logical (meaning use tabs if appropriate) spaces.
1261
1262 def reindent_to(self, column):
1263 text = self.text
1264 text.undo_block_start()
1265 if text.compare("insert linestart", "!=", "insert"):
1266 text.delete("insert linestart", "insert")
1267 if column:
1268 text.insert("insert", self._make_blanks(column))
1269 text.undo_block_stop()
1270
1271 def _asktabwidth(self):
1272 return self.askinteger(
1273 "Tab width",
1274 "Spaces per tab? (2-16)",
1275 parent=self.text,
1276 initialvalue=self.indentwidth,
1277 minvalue=2,
1278 maxvalue=16) or self.tabwidth
1279
1280 # Guess indentwidth from text content.
1281 # Return guessed indentwidth. This should not be believed unless
1282 # it's in a reasonable range (e.g., it will be 0 if no indented
1283 # blocks are found).
1284
1285 def guess_indent(self):
1286 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1287 if opener and indented:
1288 raw, indentsmall = classifyws(opener, self.tabwidth)
1289 raw, indentlarge = classifyws(indented, self.tabwidth)
1290 else:
1291 indentsmall = indentlarge = 0
1292 return indentlarge - indentsmall
1293
1294# "line.col" -> line, as an int
1295def index2line(index):
1296 return int(float(index))
1297
1298# Look at the leading whitespace in s.
1299# Return pair (# of leading ws characters,
1300# effective # of leading blanks after expanding
1301# tabs to width tabwidth)
1302
1303def classifyws(s, tabwidth):
1304 raw = effective = 0
1305 for ch in s:
1306 if ch == ' ':
1307 raw = raw + 1
1308 effective = effective + 1
1309 elif ch == '\t':
1310 raw = raw + 1
1311 effective = (effective // tabwidth + 1) * tabwidth
1312 else:
1313 break
1314 return raw, effective
1315
1316import tokenize
1317_tokenize = tokenize
1318del tokenize
1319
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +00001320class IndentSearcher(object):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001321
1322 # .run() chews over the Text widget, looking for a block opener
1323 # and the stmt following it. Returns a pair,
1324 # (line containing block opener, line containing stmt)
1325 # Either or both may be None.
1326
1327 def __init__(self, text, tabwidth):
1328 self.text = text
1329 self.tabwidth = tabwidth
1330 self.i = self.finished = 0
1331 self.blkopenline = self.indentedline = None
1332
1333 def readline(self):
1334 if self.finished:
1335 return ""
1336 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001337 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001338 if self.text.compare(mark, ">=", "end"):
1339 return ""
1340 return self.text.get(mark, mark + " lineend+1c")
1341
1342 def tokeneater(self, type, token, start, end, line,
1343 INDENT=_tokenize.INDENT,
1344 NAME=_tokenize.NAME,
1345 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1346 if self.finished:
1347 pass
1348 elif type == NAME and token in OPENERS:
1349 self.blkopenline = line
1350 elif type == INDENT and self.blkopenline:
1351 self.indentedline = line
1352 self.finished = 1
1353
1354 def run(self):
1355 save_tabsize = _tokenize.tabsize
1356 _tokenize.tabsize = self.tabwidth
1357 try:
1358 try:
1359 _tokenize.tokenize(self.readline, self.tokeneater)
1360 except _tokenize.TokenError:
1361 # since we cut off the tokenizer early, we can trigger
1362 # spurious errors
1363 pass
1364 finally:
1365 _tokenize.tabsize = save_tabsize
1366 return self.blkopenline, self.indentedline
1367
1368### end autoindent code ###
1369
David Scherer7aced172000-08-15 01:13:23 +00001370def prepstr(s):
1371 # Helper to extract the underscore from a string, e.g.
1372 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001373 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001374 if i >= 0:
1375 s = s[:i] + s[i+1:]
1376 return i, s
1377
1378
1379keynames = {
1380 'bracketleft': '[',
1381 'bracketright': ']',
1382 'slash': '/',
1383}
1384
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001385def get_accelerator(keydefs, eventname):
1386 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001387 if not keylist:
1388 return ""
1389 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001390 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001391 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1392 s = re.sub("Key-", "", s)
1393 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1394 s = re.sub("Control-", "Ctrl-", s)
1395 s = re.sub("-", "+", s)
1396 s = re.sub("><", " ", s)
1397 s = re.sub("<", "", s)
1398 s = re.sub(">", "", s)
1399 return s
1400
1401
1402def fixwordbreaks(root):
1403 # Make sure that Tk's double-click and next/previous word
1404 # operations use our definition of a word (i.e. an identifier)
1405 tk = root.tk
1406 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1407 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1408 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1409
1410
1411def test():
1412 root = Tk()
1413 fixwordbreaks(root)
1414 root.withdraw()
1415 if sys.argv[1:]:
1416 filename = sys.argv[1]
1417 else:
1418 filename = None
1419 edit = EditorWindow(root=root, filename=filename)
1420 edit.set_close_hook(root.quit)
1421 root.mainloop()
1422 root.destroy()
1423
1424if __name__ == '__main__':
1425 test()