blob: 1c04d76a937d8646db7cc86638421372fb1323da [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. Kaiserb1754452005-11-18 22:05:48 +00009from MultiCall import MultiCallCreator
Kurt B. Kaiserfd182cd2001-07-14 03:58:25 +000010
11import webbrowser
David Scherer7aced172000-08-15 01:13:23 +000012import idlever
13import WindowList
Steven M. Gavac5976402002-01-04 03:06:08 +000014import SearchDialog
15import GrepDialog
16import ReplaceDialog
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +000017import PyParse
Steven M. Gavadc72f482002-01-03 11:51:07 +000018from configHandler import idleConf
Steven M. Gava3b55a892001-11-21 05:56:26 +000019import aboutDialog, textView, configDialog
David Scherer7aced172000-08-15 01:13:23 +000020
21# The default tab setting for a Text widget, in average-width characters.
22TK_TABWIDTH_DEFAULT = 8
23
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000024def _find_module(fullname, path=None):
25 """Version of imp.find_module() that handles hierarchical module names"""
26
27 file = None
28 for tgt in fullname.split('.'):
29 if file is not None:
30 file.close() # close intermediate files
31 (file, filename, descr) = imp.find_module(tgt, path)
32 if descr[2] == imp.PY_SOURCE:
33 break # find but not load the source file
34 module = imp.load_module(tgt, file, filename, descr)
Kurt B. Kaiser69e8afc2003-01-10 21:25:20 +000035 try:
36 path = module.__path__
37 except AttributeError:
38 raise ImportError, 'No source for module ' + module.__name__
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +000039 return file, filename, descr
40
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +000041class EditorWindow(object):
David Scherer7aced172000-08-15 01:13:23 +000042 from Percolator import Percolator
43 from ColorDelegator import ColorDelegator
44 from UndoDelegator import UndoDelegator
45 from IOBinding import IOBinding
46 import Bindings
47 from Tkinter import Toplevel
48 from MultiStatusBar import MultiStatusBar
49
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000050 help_url = None
David Scherer7aced172000-08-15 01:13:23 +000051
52 def __init__(self, flist=None, filename=None, key=None, root=None):
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000053 if EditorWindow.help_url is None:
Thomas Heller84ef1532003-09-23 20:53:10 +000054 dochome = os.path.join(sys.prefix, 'Doc', 'index.html')
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000055 if sys.platform.count('linux'):
56 # look for html docs in a couple of standard places
57 pyver = 'python-docs-' + '%s.%s.%s' % sys.version_info[:3]
58 if os.path.isdir('/var/www/html/python/'): # "python2" rpm
59 dochome = '/var/www/html/python/index.html'
60 else:
61 basepath = '/usr/share/doc/' # standard location
62 dochome = os.path.join(basepath, pyver,
63 'Doc', 'index.html')
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +000064 elif sys.platform[:3] == 'win':
Kurt B. Kaiser090e6362004-07-21 03:33:58 +000065 chmfile = os.path.join(sys.prefix, 'Doc',
66 'Python%d%d.chm' % sys.version_info[:2])
Thomas Heller84ef1532003-09-23 20:53:10 +000067 if os.path.isfile(chmfile):
68 dochome = chmfile
Kurt B. Kaiser114713d2003-01-10 05:07:24 +000069 dochome = os.path.normpath(dochome)
70 if os.path.isfile(dochome):
71 EditorWindow.help_url = dochome
72 else:
73 EditorWindow.help_url = "http://www.python.org/doc/current"
Steven M. Gavadc72f482002-01-03 11:51:07 +000074 currentTheme=idleConf.CurrentTheme()
David Scherer7aced172000-08-15 01:13:23 +000075 self.flist = flist
76 root = root or flist.root
77 self.root = root
David Scherer7aced172000-08-15 01:13:23 +000078 self.menubar = Menu(root)
Kurt B. Kaiser183403a2004-08-22 05:14:32 +000079 self.top = top = WindowList.ListedToplevel(root, menu=self.menubar)
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000080 if flist:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000081 self.tkinter_vars = flist.vars
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000082 #self.top.instance_dict makes flist.inversedict avalable to
Steven M. Gava0c5bc8c2002-03-27 02:25:44 +000083 #configDialog.py so it can access all EditorWindow instaces
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000084 self.top.instance_dict=flist.inversedict
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +000085 else:
86 self.tkinter_vars = {} # keys: Tkinter event names
87 # values: Tkinter variable instances
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +000088 self.recent_files_path=os.path.join(idleConf.GetUserCfgDir(),
Steven M. Gava1d46e402002-03-27 08:40:46 +000089 'recent-files.lst')
David Scherer7aced172000-08-15 01:13:23 +000090 self.vbar = vbar = Scrollbar(top, name='vbar')
91 self.text_frame = text_frame = Frame(top)
Kurt B. Kaiser1061e722003-01-04 01:43:53 +000092 self.width = idleConf.GetOption('main','EditorWindow','width')
Kurt B. Kaiserb1754452005-11-18 22:05:48 +000093 self.text = text = MultiCallCreator(Text)(
94 text_frame, name='text', padx=5, wrap='none',
Steven M. Gavadc72f482002-01-03 11:51:07 +000095 foreground=idleConf.GetHighlight(currentTheme,
96 'normal',fgBg='fg'),
97 background=idleConf.GetHighlight(currentTheme,
98 'normal',fgBg='bg'),
99 highlightcolor=idleConf.GetHighlight(currentTheme,
100 'hilite',fgBg='fg'),
101 highlightbackground=idleConf.GetHighlight(currentTheme,
102 'hilite',fgBg='bg'),
103 insertbackground=idleConf.GetHighlight(currentTheme,
104 'cursor',fgBg='fg'),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000105 width=self.width,
Steven M. Gavadc72f482002-01-03 11:51:07 +0000106 height=idleConf.GetOption('main','EditorWindow','height') )
Kurt B. Kaiser183403a2004-08-22 05:14:32 +0000107 self.top.focused_widget = self.text
David Scherer7aced172000-08-15 01:13:23 +0000108
109 self.createmenubar()
110 self.apply_bindings()
111
112 self.top.protocol("WM_DELETE_WINDOW", self.close)
113 self.top.bind("<<close-window>>", self.close_event)
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000114 text.bind("<<cut>>", self.cut)
115 text.bind("<<copy>>", self.copy)
116 text.bind("<<paste>>", self.paste)
David Scherer7aced172000-08-15 01:13:23 +0000117 text.bind("<<center-insert>>", self.center_insert_event)
118 text.bind("<<help>>", self.help_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000119 text.bind("<<python-docs>>", self.python_docs)
120 text.bind("<<about-idle>>", self.about_dialog)
Steven M. Gava3b55a892001-11-21 05:56:26 +0000121 text.bind("<<open-config-dialog>>", self.config_dialog)
David Scherer7aced172000-08-15 01:13:23 +0000122 text.bind("<<open-module>>", self.open_module)
123 text.bind("<<do-nothing>>", lambda event: "break")
124 text.bind("<<select-all>>", self.select_all)
125 text.bind("<<remove-selection>>", self.remove_selection)
Steven M. Gavac5976402002-01-04 03:06:08 +0000126 text.bind("<<find>>", self.find_event)
127 text.bind("<<find-again>>", self.find_again_event)
128 text.bind("<<find-in-files>>", self.find_in_files_event)
129 text.bind("<<find-selection>>", self.find_selection_event)
130 text.bind("<<replace>>", self.replace_event)
131 text.bind("<<goto-line>>", self.goto_line_event)
David Scherer7aced172000-08-15 01:13:23 +0000132 text.bind("<3>", self.right_menu_event)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000133 text.bind("<<smart-backspace>>",self.smart_backspace_event)
134 text.bind("<<newline-and-indent>>",self.newline_and_indent_event)
135 text.bind("<<smart-indent>>",self.smart_indent_event)
136 text.bind("<<indent-region>>",self.indent_region_event)
137 text.bind("<<dedent-region>>",self.dedent_region_event)
138 text.bind("<<comment-region>>",self.comment_region_event)
139 text.bind("<<uncomment-region>>",self.uncomment_region_event)
140 text.bind("<<tabify-region>>",self.tabify_region_event)
141 text.bind("<<untabify-region>>",self.untabify_region_event)
142 text.bind("<<toggle-tabs>>",self.toggle_tabs_event)
143 text.bind("<<change-indentwidth>>",self.change_indentwidth_event)
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000144 text.bind("<Left>", self.move_at_edge_if_selection(0))
145 text.bind("<Right>", self.move_at_edge_if_selection(1))
Kurt B. Kaiser3069dbb2005-01-28 00:16:16 +0000146 text.bind("<<del-word-left>>", self.del_word_left)
147 text.bind("<<del-word-right>>", self.del_word_right)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000148
David Scherer7aced172000-08-15 01:13:23 +0000149 if flist:
150 flist.inversedict[self] = key
151 if key:
152 flist.dict[key] = self
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000153 text.bind("<<open-new-window>>", self.new_callback)
David Scherer7aced172000-08-15 01:13:23 +0000154 text.bind("<<close-all-windows>>", self.flist.close_all_callback)
155 text.bind("<<open-class-browser>>", self.open_class_browser)
156 text.bind("<<open-path-browser>>", self.open_path_browser)
157
Steven M. Gava898a3652001-10-07 11:10:44 +0000158 self.set_status_bar()
David Scherer7aced172000-08-15 01:13:23 +0000159 vbar['command'] = text.yview
160 vbar.pack(side=RIGHT, fill=Y)
David Scherer7aced172000-08-15 01:13:23 +0000161 text['yscrollcommand'] = vbar.set
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000162 fontWeight = 'normal'
163 if idleConf.GetOption('main', 'EditorWindow', 'font-bold', type='bool'):
Steven M. Gavab1585412002-03-12 00:21:56 +0000164 fontWeight='bold'
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000165 text.config(font=(idleConf.GetOption('main', 'EditorWindow', 'font'),
166 idleConf.GetOption('main', 'EditorWindow', 'font-size'),
167 fontWeight))
David Scherer7aced172000-08-15 01:13:23 +0000168 text_frame.pack(side=LEFT, fill=BOTH, expand=1)
169 text.pack(side=TOP, fill=BOTH, expand=1)
170 text.focus_set()
171
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000172 # usetabs true -> literal tab characters are used by indent and
173 # dedent cmds, possibly mixed with spaces if
174 # indentwidth is not a multiple of tabwidth,
175 # which will cause Tabnanny to nag!
176 # false -> tab characters are converted to spaces by indent
177 # and dedent cmds, and ditto TAB keystrokes
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000178 # Although use-spaces=0 can be configured manually in config-main.def,
179 # configuration of tabs v. spaces is not supported in the configuration
180 # dialog. IDLE promotes the preferred Python indentation: use spaces!
181 usespaces = idleConf.GetOption('main', 'Indent', 'use-spaces', type='bool')
182 self.usetabs = not usespaces
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000183
184 # tabwidth is the display width of a literal tab character.
185 # CAUTION: telling Tk to use anything other than its default
186 # tab setting causes it to use an entirely different tabbing algorithm,
187 # treating tab stops as fixed distances from the left margin.
188 # Nobody expects this, so for now tabwidth should never be changed.
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000189 self.tabwidth = 8 # must remain 8 until Tk is fixed.
190
191 # indentwidth is the number of screen characters per indent level.
192 # The recommended Python indentation is four spaces.
193 self.indentwidth = self.tabwidth
194 self.set_notabs_indentwidth()
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000195
196 # If context_use_ps1 is true, parsing searches back for a ps1 line;
197 # else searches for a popular (if, def, ...) Python stmt.
198 self.context_use_ps1 = False
199
200 # When searching backwards for a reliable place to begin parsing,
201 # first start num_context_lines[0] lines back, then
202 # num_context_lines[1] lines back if that didn't work, and so on.
203 # The last value should be huge (larger than the # of lines in a
204 # conceivable file).
205 # Making the initial values larger slows things down more often.
206 self.num_context_lines = 50, 500, 5000000
207
David Scherer7aced172000-08-15 01:13:23 +0000208 self.per = per = self.Percolator(text)
209 if self.ispythonsource(filename):
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000210 self.color = color = self.ColorDelegator()
211 per.insertfilter(color)
David Scherer7aced172000-08-15 01:13:23 +0000212 else:
David Scherer7aced172000-08-15 01:13:23 +0000213 self.color = None
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000214
215 self.undo = undo = self.UndoDelegator()
216 per.insertfilter(undo)
217 text.undo_block_start = undo.undo_block_start
218 text.undo_block_stop = undo.undo_block_stop
219 undo.set_saved_change_hook(self.saved_change_hook)
220
221 # IOBinding implements file I/O and printing functionality
David Scherer7aced172000-08-15 01:13:23 +0000222 self.io = io = self.IOBinding(self)
Kurt B. Kaiserdc1e7092002-07-11 04:33:41 +0000223 io.set_filename_change_hook(self.filename_change_hook)
224
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000225 # Create the recent files submenu
226 self.recent_files_menu = Menu(self.menubar)
227 self.menudict['file'].insert_cascade(3, label='Recent Files',
228 underline=0,
229 menu=self.recent_files_menu)
230 self.update_recent_files_list()
David Scherer7aced172000-08-15 01:13:23 +0000231
David Scherer7aced172000-08-15 01:13:23 +0000232 if filename:
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000233 if os.path.exists(filename) and not os.path.isdir(filename):
David Scherer7aced172000-08-15 01:13:23 +0000234 io.loadfile(filename)
235 else:
236 io.set_filename(filename)
David Scherer7aced172000-08-15 01:13:23 +0000237 self.saved_change_hook()
238
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000239 self.set_indentation_params(self.ispythonsource(filename))
240
David Scherer7aced172000-08-15 01:13:23 +0000241 self.load_extensions()
242
243 menu = self.menudict.get('windows')
244 if menu:
245 end = menu.index("end")
246 if end is None:
247 end = -1
248 if end >= 0:
249 menu.add_separator()
250 end = end + 1
251 self.wmenu_end = end
252 WindowList.register_callback(self.postwindowsmenu)
253
254 # Some abstractions so IDLE extensions are cross-IDE
255 self.askyesno = tkMessageBox.askyesno
256 self.askinteger = tkSimpleDialog.askinteger
257 self.showerror = tkMessageBox.showerror
258
Kurt B. Kaiserd2f48612003-06-05 02:34:04 +0000259 def new_callback(self, event):
260 dirname, basename = self.io.defaultfilename()
261 self.flist.new(dirname)
262 return "break"
263
David Scherer7aced172000-08-15 01:13:23 +0000264 def set_status_bar(self):
Steven M. Gava898a3652001-10-07 11:10:44 +0000265 self.status_bar = self.MultiStatusBar(self.top)
David Scherer7aced172000-08-15 01:13:23 +0000266 self.status_bar.set_label('column', 'Col: ?', side=RIGHT)
267 self.status_bar.set_label('line', 'Ln: ?', side=RIGHT)
268 self.status_bar.pack(side=BOTTOM, fill=X)
Kurt B. Kaiserb1754452005-11-18 22:05:48 +0000269 self.text.bind("<<set-line-and-column>>", self.set_line_and_column)
270 self.text.event_add("<<set-line-and-column>>",
271 "<KeyRelease>", "<ButtonRelease>")
David Scherer7aced172000-08-15 01:13:23 +0000272 self.text.after_idle(self.set_line_and_column)
273
274 def set_line_and_column(self, event=None):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000275 line, column = self.text.index(INSERT).split('.')
David Scherer7aced172000-08-15 01:13:23 +0000276 self.status_bar.set_label('column', 'Col: %s' % column)
277 self.status_bar.set_label('line', 'Ln: %s' % line)
278
David Scherer7aced172000-08-15 01:13:23 +0000279 menu_specs = [
280 ("file", "_File"),
281 ("edit", "_Edit"),
282 ("format", "F_ormat"),
283 ("run", "_Run"),
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000284 ("options", "_Options"),
David Scherer7aced172000-08-15 01:13:23 +0000285 ("windows", "_Windows"),
286 ("help", "_Help"),
287 ]
288
289 def createmenubar(self):
290 mbar = self.menubar
291 self.menudict = menudict = {}
292 for name, label in self.menu_specs:
293 underline, label = prepstr(label)
294 menudict[name] = menu = Menu(mbar, name=name)
295 mbar.add_cascade(label=label, menu=menu, underline=underline)
296 self.fill_menus()
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000297 self.base_helpmenu_length = self.menudict['help'].index(END)
298 self.reset_help_menu_entries()
David Scherer7aced172000-08-15 01:13:23 +0000299
300 def postwindowsmenu(self):
301 # Only called when Windows menu exists
David Scherer7aced172000-08-15 01:13:23 +0000302 menu = self.menudict['windows']
303 end = menu.index("end")
304 if end is None:
305 end = -1
306 if end > self.wmenu_end:
307 menu.delete(self.wmenu_end+1, end)
308 WindowList.add_windows_to_menu(menu)
309
310 rmenu = None
311
312 def right_menu_event(self, event):
313 self.text.tag_remove("sel", "1.0", "end")
314 self.text.mark_set("insert", "@%d,%d" % (event.x, event.y))
315 if not self.rmenu:
316 self.make_rmenu()
317 rmenu = self.rmenu
318 self.event = event
319 iswin = sys.platform[:3] == 'win'
320 if iswin:
321 self.text.config(cursor="arrow")
322 rmenu.tk_popup(event.x_root, event.y_root)
323 if iswin:
324 self.text.config(cursor="ibeam")
325
326 rmenu_specs = [
327 # ("Label", "<<virtual-event>>"), ...
328 ("Close", "<<close-window>>"), # Example
329 ]
330
331 def make_rmenu(self):
332 rmenu = Menu(self.text, tearoff=0)
333 for label, eventname in self.rmenu_specs:
334 def command(text=self.text, eventname=eventname):
335 text.event_generate(eventname)
336 rmenu.add_command(label=label, command=command)
337 self.rmenu = rmenu
338
339 def about_dialog(self, event=None):
Kurt B. Kaiserd78b2302003-06-12 04:03:49 +0000340 aboutDialog.AboutDialog(self.top,'About IDLE')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000341
Steven M. Gava3b55a892001-11-21 05:56:26 +0000342 def config_dialog(self, event=None):
343 configDialog.ConfigDialog(self.top,'Settings')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000344
David Scherer7aced172000-08-15 01:13:23 +0000345 def help_dialog(self, event=None):
Steven M. Gavab9d07b52001-07-31 11:11:38 +0000346 fn=os.path.join(os.path.abspath(os.path.dirname(__file__)),'help.txt')
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000347 textView.TextViewer(self.top,'Help',fn)
348
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000349 def python_docs(self, event=None):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000350 if sys.platform[:3] == 'win':
Steven M. Gava931625d2002-04-22 00:38:26 +0000351 os.startfile(self.help_url)
Kurt B. Kaiser114713d2003-01-10 05:07:24 +0000352 else:
353 webbrowser.open(self.help_url)
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000354 return "break"
David Scherer7aced172000-08-15 01:13:23 +0000355
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000356 def cut(self,event):
357 self.text.event_generate("<<Cut>>")
358 return "break"
359
360 def copy(self,event):
Kurt B. Kaiserb1754452005-11-18 22:05:48 +0000361 if not self.text.tag_ranges("sel"):
362 # There is no selection, so do nothing and maybe interrupt.
363 return
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000364 self.text.event_generate("<<Copy>>")
365 return "break"
366
367 def paste(self,event):
368 self.text.event_generate("<<Paste>>")
369 return "break"
370
David Scherer7aced172000-08-15 01:13:23 +0000371 def select_all(self, event=None):
372 self.text.tag_add("sel", "1.0", "end-1c")
373 self.text.mark_set("insert", "1.0")
374 self.text.see("insert")
375 return "break"
376
377 def remove_selection(self, event=None):
378 self.text.tag_remove("sel", "1.0", "end")
379 self.text.see("insert")
380
Kurt B. Kaiser5ec186b2003-01-17 04:04:06 +0000381 def move_at_edge_if_selection(self, edge_index):
382 """Cursor move begins at start or end of selection
383
384 When a left/right cursor key is pressed create and return to Tkinter a
385 function which causes a cursor move from the associated edge of the
386 selection.
387
388 """
389 self_text_index = self.text.index
390 self_text_mark_set = self.text.mark_set
391 edges_table = ("sel.first+1c", "sel.last-1c")
392 def move_at_edge(event):
393 if (event.state & 5) == 0: # no shift(==1) or control(==4) pressed
394 try:
395 self_text_index("sel.first")
396 self_text_mark_set("insert", edges_table[edge_index])
397 except TclError:
398 pass
399 return move_at_edge
400
Kurt B. Kaiser3069dbb2005-01-28 00:16:16 +0000401 def del_word_left(self, event):
402 self.text.event_generate('<Meta-Delete>')
403 return "break"
404
405 def del_word_right(self, event):
406 self.text.event_generate('<Meta-d>')
407 return "break"
408
Steven M. Gavac5976402002-01-04 03:06:08 +0000409 def find_event(self, event):
410 SearchDialog.find(self.text)
411 return "break"
412
413 def find_again_event(self, event):
414 SearchDialog.find_again(self.text)
415 return "break"
416
417 def find_selection_event(self, event):
418 SearchDialog.find_selection(self.text)
419 return "break"
420
421 def find_in_files_event(self, event):
422 GrepDialog.grep(self.text, self.io, self.flist)
423 return "break"
424
425 def replace_event(self, event):
426 ReplaceDialog.replace(self.text)
427 return "break"
428
429 def goto_line_event(self, event):
430 text = self.text
431 lineno = tkSimpleDialog.askinteger("Goto",
432 "Go to line number:",parent=text)
433 if lineno is None:
434 return "break"
435 if lineno <= 0:
436 text.bell()
437 return "break"
438 text.mark_set("insert", "%d.0" % lineno)
439 text.see("insert")
440
David Scherer7aced172000-08-15 01:13:23 +0000441 def open_module(self, event=None):
442 # XXX Shouldn't this be in IOBinding or in FileList?
443 try:
444 name = self.text.get("sel.first", "sel.last")
445 except TclError:
446 name = ""
447 else:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000448 name = name.strip()
Guido van Rossum852f35b2003-06-05 11:36:55 +0000449 name = tkSimpleDialog.askstring("Module",
450 "Enter the name of a Python module\n"
451 "to search on sys.path and open:",
452 parent=self.text, initialvalue=name)
453 if name:
454 name = name.strip()
David Scherer7aced172000-08-15 01:13:23 +0000455 if not name:
Guido van Rossum852f35b2003-06-05 11:36:55 +0000456 return
David Scherer7aced172000-08-15 01:13:23 +0000457 # XXX Ought to insert current file's directory in front of path
458 try:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000459 (f, file, (suffix, mode, type)) = _find_module(name)
David Scherer7aced172000-08-15 01:13:23 +0000460 except (NameError, ImportError), msg:
461 tkMessageBox.showerror("Import error", str(msg), parent=self.text)
462 return
463 if type != imp.PY_SOURCE:
464 tkMessageBox.showerror("Unsupported type",
465 "%s is not a source module" % name, parent=self.text)
466 return
467 if f:
468 f.close()
469 if self.flist:
470 self.flist.open(file)
471 else:
472 self.io.loadfile(file)
473
474 def open_class_browser(self, event=None):
475 filename = self.io.filename
476 if not filename:
477 tkMessageBox.showerror(
478 "No filename",
479 "This buffer has no associated filename",
480 master=self.text)
481 self.text.focus_set()
482 return None
483 head, tail = os.path.split(filename)
484 base, ext = os.path.splitext(tail)
485 import ClassBrowser
486 ClassBrowser.ClassBrowser(self.flist, base, [head])
487
488 def open_path_browser(self, event=None):
489 import PathBrowser
490 PathBrowser.PathBrowser(self.flist)
491
492 def gotoline(self, lineno):
493 if lineno is not None and lineno > 0:
494 self.text.mark_set("insert", "%d.0" % lineno)
495 self.text.tag_remove("sel", "1.0", "end")
496 self.text.tag_add("sel", "insert", "insert +1l")
497 self.center()
498
499 def ispythonsource(self, filename):
Kurt B. Kaiserdf506ea2005-06-12 04:33:30 +0000500 if not filename or os.path.isdir(filename):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000501 return True
David Scherer7aced172000-08-15 01:13:23 +0000502 base, ext = os.path.splitext(os.path.basename(filename))
503 if os.path.normcase(ext) in (".py", ".pyw"):
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000504 return True
David Scherer7aced172000-08-15 01:13:23 +0000505 try:
506 f = open(filename)
507 line = f.readline()
508 f.close()
509 except IOError:
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000510 return False
Kurt B. Kaiser83a35602002-12-20 17:18:03 +0000511 return line.startswith('#!') and line.find('python') >= 0
David Scherer7aced172000-08-15 01:13:23 +0000512
513 def close_hook(self):
514 if self.flist:
515 self.flist.close_edit(self)
516
517 def set_close_hook(self, close_hook):
518 self.close_hook = close_hook
519
520 def filename_change_hook(self):
521 if self.flist:
522 self.flist.filename_changed_edit(self)
523 self.saved_change_hook()
Kurt B. Kaiser260cb902003-06-06 21:58:38 +0000524 self.top.update_windowlist_registry(self)
David Scherer7aced172000-08-15 01:13:23 +0000525 if self.ispythonsource(self.io.filename):
526 self.addcolorizer()
527 else:
528 self.rmcolorizer()
529
530 def addcolorizer(self):
531 if self.color:
532 return
David Scherer7aced172000-08-15 01:13:23 +0000533 self.per.removefilter(self.undo)
534 self.color = self.ColorDelegator()
535 self.per.insertfilter(self.color)
536 self.per.insertfilter(self.undo)
537
538 def rmcolorizer(self):
539 if not self.color:
540 return
Kurt B. Kaiserdf506ea2005-06-12 04:33:30 +0000541 self.color.removecolors()
David Scherer7aced172000-08-15 01:13:23 +0000542 self.per.removefilter(self.undo)
543 self.per.removefilter(self.color)
544 self.color = None
545 self.per.insertfilter(self.undo)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000546
Steven M. Gavab77d3432002-03-02 07:16:21 +0000547 def ResetColorizer(self):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000548 "Update the colour theme if it is changed"
549 # Called from configDialog.py
Steven M. Gavab77d3432002-03-02 07:16:21 +0000550 if self.color:
551 self.color = self.ColorDelegator()
552 self.per.insertfilter(self.color)
Kurt B. Kaiser73360a32004-03-08 18:15:31 +0000553 theme = idleConf.GetOption('main','Theme','name')
554 self.text.config(idleConf.GetHighlight(theme, "normal"))
David Scherer7aced172000-08-15 01:13:23 +0000555
Steven M. Gavab1585412002-03-12 00:21:56 +0000556 def ResetFont(self):
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000557 "Update the text widgets' font if it is changed"
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000558 # Called from configDialog.py
Steven M. Gavab1585412002-03-12 00:21:56 +0000559 fontWeight='normal'
560 if idleConf.GetOption('main','EditorWindow','font-bold',type='bool'):
561 fontWeight='bold'
562 self.text.config(font=(idleConf.GetOption('main','EditorWindow','font'),
563 idleConf.GetOption('main','EditorWindow','font-size'),
564 fontWeight))
565
Kurt B. Kaiserb1754452005-11-18 22:05:48 +0000566 def RemoveKeybindings(self):
567 "Remove the keybindings before they are changed."
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000568 # Called from configDialog.py
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000569 self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000570 for event, keylist in keydefs.items():
Kurt B. Kaiserb1754452005-11-18 22:05:48 +0000571 self.text.event_delete(event, *keylist)
572 for extensionName in self.get_standard_extension_names():
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000573 xkeydefs = idleConf.GetExtensionBindings(extensionName)
574 if xkeydefs:
575 for event, keylist in xkeydefs.items():
Kurt B. Kaiserb1754452005-11-18 22:05:48 +0000576 self.text.event_delete(event, *keylist)
577
578 def ApplyKeybindings(self):
579 "Update the keybindings after they are changed"
580 # Called from configDialog.py
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000581 self.Bindings.default_keydefs = keydefs = idleConf.GetCurrentKeySet()
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000582 self.apply_bindings()
Kurt B. Kaiserb1754452005-11-18 22:05:48 +0000583 for extensionName in self.get_standard_extension_names():
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000584 xkeydefs = idleConf.GetExtensionBindings(extensionName)
585 if xkeydefs:
586 self.apply_bindings(xkeydefs)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000587 #update menu accelerators
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000588 menuEventDict = {}
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000589 for menu in self.Bindings.menudefs:
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000590 menuEventDict[menu[0]] = {}
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000591 for item in menu[1]:
592 if item:
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000593 menuEventDict[menu[0]][prepstr(item[0])[1]] = item[1]
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000594 for menubarItem in self.menudict.keys():
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000595 menu = self.menudict[menubarItem]
596 end = menu.index(END) + 1
597 for index in range(0, end):
598 if menu.type(index) == 'command':
599 accel = menu.entrycget(index, 'accelerator')
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000600 if accel:
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000601 itemName = menu.entrycget(index, 'label')
602 event = ''
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000603 if menuEventDict.has_key(menubarItem):
604 if menuEventDict[menubarItem].has_key(itemName):
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000605 event = menuEventDict[menubarItem][itemName]
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000606 if event:
Kurt B. Kaiser5a67f9b2005-11-22 01:47:14 +0000607 accel = get_accelerator(keydefs, event)
608 menu.entryconfig(index, accelerator=accel)
Steven M. Gavadbfe92c2002-03-18 02:38:44 +0000609
Kurt B. Kaiseracdef852005-01-31 03:34:26 +0000610 def set_notabs_indentwidth(self):
611 "Update the indentwidth if changed and not using tabs in this window"
612 # Called from configDialog.py
613 if not self.usetabs:
614 self.indentwidth = idleConf.GetOption('main', 'Indent','num-spaces',
615 type='int')
616
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000617 def reset_help_menu_entries(self):
618 "Update the additional help entries on the Help menu"
619 help_list = idleConf.GetAllExtraHelpSourcesList()
620 helpmenu = self.menudict['help']
621 # first delete the extra help entries, if any
622 helpmenu_length = helpmenu.index(END)
623 if helpmenu_length > self.base_helpmenu_length:
624 helpmenu.delete((self.base_helpmenu_length + 1), helpmenu_length)
625 # then rebuild them
626 if help_list:
627 helpmenu.add_separator()
628 for entry in help_list:
629 cmd = self.__extra_help_callback(entry[1])
630 helpmenu.add_command(label=entry[0], command=cmd)
631 # and update the menu dictionary
632 self.menudict['help'] = helpmenu
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000633
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000634 def __extra_help_callback(self, helpfile):
635 "Create a callback with the helpfile value frozen at definition time"
636 def display_extra_help(helpfile=helpfile):
Kurt B. Kaiser8aa23922004-07-15 04:54:57 +0000637 if not (helpfile.startswith('www') or helpfile.startswith('http')):
638 url = os.path.normpath(helpfile)
639 if sys.platform[:3] == 'win':
640 os.startfile(helpfile)
641 else:
642 webbrowser.open(helpfile)
Kurt B. Kaiser8e92bf72003-01-14 22:03:31 +0000643 return display_extra_help
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000644
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000645 def update_recent_files_list(self, new_file=None):
646 "Load and update the recent files list and menus"
647 rf_list = []
648 if os.path.exists(self.recent_files_path):
649 rf_list_file = open(self.recent_files_path,'r')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000650 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000651 rf_list = rf_list_file.readlines()
Steven M. Gava1d46e402002-03-27 08:40:46 +0000652 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000653 rf_list_file.close()
654 if new_file:
655 new_file = os.path.abspath(new_file) + '\n'
656 if new_file in rf_list:
657 rf_list.remove(new_file) # move to top
658 rf_list.insert(0, new_file)
659 # clean and save the recent files list
660 bad_paths = []
661 for path in rf_list:
662 if '\0' in path or not os.path.exists(path[0:-1]):
663 bad_paths.append(path)
664 rf_list = [path for path in rf_list if path not in bad_paths]
665 ulchars = "1234567890ABCDEFGHIJK"
666 rf_list = rf_list[0:len(ulchars)]
667 rf_file = open(self.recent_files_path, 'w')
Steven M. Gava1d46e402002-03-27 08:40:46 +0000668 try:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000669 rf_file.writelines(rf_list)
Steven M. Gava1d46e402002-03-27 08:40:46 +0000670 finally:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000671 rf_file.close()
672 # for each edit window instance, construct the recent files menu
673 for instance in self.top.instance_dict.keys():
674 menu = instance.recent_files_menu
675 menu.delete(1, END) # clear, and rebuild:
676 for i, file in zip(count(), rf_list):
677 file_name = file[0:-1] # zap \n
678 callback = instance.__recent_file_callback(file_name)
679 menu.add_command(label=ulchars[i] + " " + file_name,
680 command=callback,
681 underline=0)
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000682
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000683 def __recent_file_callback(self, file_name):
684 def open_recent_file(fn_closure=file_name):
685 self.io.open(editFile=fn_closure)
686 return open_recent_file
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000687
David Scherer7aced172000-08-15 01:13:23 +0000688 def saved_change_hook(self):
689 short = self.short_title()
690 long = self.long_title()
691 if short and long:
692 title = short + " - " + long
693 elif short:
694 title = short
695 elif long:
696 title = long
697 else:
698 title = "Untitled"
699 icon = short or long or title
700 if not self.get_saved():
701 title = "*%s*" % title
702 icon = "*%s" % icon
703 self.top.wm_title(title)
704 self.top.wm_iconname(icon)
705
706 def get_saved(self):
707 return self.undo.get_saved()
708
709 def set_saved(self, flag):
710 self.undo.set_saved(flag)
711
712 def reset_undo(self):
713 self.undo.reset_undo()
714
715 def short_title(self):
716 filename = self.io.filename
717 if filename:
718 filename = os.path.basename(filename)
719 return filename
720
721 def long_title(self):
722 return self.io.filename or ""
723
724 def center_insert_event(self, event):
725 self.center()
726
727 def center(self, mark="insert"):
728 text = self.text
729 top, bot = self.getwindowlines()
730 lineno = self.getlineno(mark)
731 height = bot - top
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000732 newtop = max(1, lineno - height//2)
David Scherer7aced172000-08-15 01:13:23 +0000733 text.yview(float(newtop))
734
735 def getwindowlines(self):
736 text = self.text
737 top = self.getlineno("@0,0")
738 bot = self.getlineno("@0,65535")
739 if top == bot and text.winfo_height() == 1:
740 # Geometry manager hasn't run yet
741 height = int(text['height'])
742 bot = top + height - 1
743 return top, bot
744
745 def getlineno(self, mark="insert"):
746 text = self.text
747 return int(float(text.index(mark)))
748
Kurt B. Kaiser1061e722003-01-04 01:43:53 +0000749 def get_geometry(self):
750 "Return (width, height, x, y)"
751 geom = self.top.wm_geometry()
752 m = re.match(r"(\d+)x(\d+)\+(-?\d+)\+(-?\d+)", geom)
753 tuple = (map(int, m.groups()))
754 return tuple
755
David Scherer7aced172000-08-15 01:13:23 +0000756 def close_event(self, event):
757 self.close()
758
759 def maybesave(self):
760 if self.io:
Steven M. Gava67716b52002-02-26 02:31:03 +0000761 if not self.get_saved():
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +0000762 if self.top.state()!='normal':
Steven M. Gava67716b52002-02-26 02:31:03 +0000763 self.top.deiconify()
764 self.top.lower()
765 self.top.lift()
David Scherer7aced172000-08-15 01:13:23 +0000766 return self.io.maybesave()
767
768 def close(self):
David Scherer7aced172000-08-15 01:13:23 +0000769 reply = self.maybesave()
770 if reply != "cancel":
771 self._close()
772 return reply
773
774 def _close(self):
Steven M. Gava1d46e402002-03-27 08:40:46 +0000775 if self.io.filename:
Kurt B. Kaisercf6f1b62004-04-11 03:16:07 +0000776 self.update_recent_files_list(new_file=self.io.filename)
David Scherer7aced172000-08-15 01:13:23 +0000777 WindowList.unregister_callback(self.postwindowsmenu)
778 if self.close_hook:
779 self.close_hook()
780 self.flist = None
781 colorizing = 0
782 self.unload_extensions()
783 self.io.close(); self.io = None
784 self.undo = None # XXX
785 if self.color:
786 colorizing = self.color.colorizing
787 doh = colorizing and self.top
788 self.color.close(doh) # Cancel colorization
789 self.text = None
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000790 self.tkinter_vars = None
David Scherer7aced172000-08-15 01:13:23 +0000791 self.per.close(); self.per = None
792 if not colorizing:
793 self.top.destroy()
794
795 def load_extensions(self):
796 self.extensions = {}
797 self.load_standard_extensions()
798
799 def unload_extensions(self):
800 for ins in self.extensions.values():
801 if hasattr(ins, "close"):
802 ins.close()
803 self.extensions = {}
804
805 def load_standard_extensions(self):
806 for name in self.get_standard_extension_names():
807 try:
808 self.load_extension(name)
809 except:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000810 print "Failed to load extension", repr(name)
David Scherer7aced172000-08-15 01:13:23 +0000811 import traceback
812 traceback.print_exc()
813
814 def get_standard_extension_names(self):
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000815 return idleConf.GetExtensions(editor_only=True)
David Scherer7aced172000-08-15 01:13:23 +0000816
817 def load_extension(self, name):
Kurt B. Kaiserb00e89f2005-01-18 00:54:58 +0000818 try:
819 mod = __import__(name, globals(), locals(), [])
820 except ImportError:
821 print "\nFailed to import extension: ", name
822 return
David Scherer7aced172000-08-15 01:13:23 +0000823 cls = getattr(mod, name)
Kurt B. Kaiser4d5bc602004-06-06 01:29:22 +0000824 keydefs = idleConf.GetExtensionBindings(name)
825 if hasattr(cls, "menudefs"):
826 self.fill_menus(cls.menudefs, keydefs)
David Scherer7aced172000-08-15 01:13:23 +0000827 ins = cls(self)
828 self.extensions[name] = ins
David Scherer7aced172000-08-15 01:13:23 +0000829 if keydefs:
830 self.apply_bindings(keydefs)
831 for vevent in keydefs.keys():
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +0000832 methodname = vevent.replace("-", "_")
David Scherer7aced172000-08-15 01:13:23 +0000833 while methodname[:1] == '<':
834 methodname = methodname[1:]
835 while methodname[-1:] == '>':
836 methodname = methodname[:-1]
837 methodname = methodname + "_event"
838 if hasattr(ins, methodname):
839 self.text.bind(vevent, getattr(ins, methodname))
David Scherer7aced172000-08-15 01:13:23 +0000840
841 def apply_bindings(self, keydefs=None):
842 if keydefs is None:
843 keydefs = self.Bindings.default_keydefs
844 text = self.text
845 text.keydefs = keydefs
846 for event, keylist in keydefs.items():
847 if keylist:
Raymond Hettinger931237e2003-07-09 18:48:24 +0000848 text.event_add(event, *keylist)
David Scherer7aced172000-08-15 01:13:23 +0000849
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000850 def fill_menus(self, menudefs=None, keydefs=None):
Kurt B. Kaiser83118c62002-06-24 17:03:37 +0000851 """Add appropriate entries to the menus and submenus
852
853 Menus that are absent or None in self.menudict are ignored.
854 """
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000855 if menudefs is None:
856 menudefs = self.Bindings.menudefs
David Scherer7aced172000-08-15 01:13:23 +0000857 if keydefs is None:
858 keydefs = self.Bindings.default_keydefs
859 menudict = self.menudict
860 text = self.text
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000861 for mname, entrylist in menudefs:
David Scherer7aced172000-08-15 01:13:23 +0000862 menu = menudict.get(mname)
863 if not menu:
864 continue
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000865 for entry in entrylist:
866 if not entry:
David Scherer7aced172000-08-15 01:13:23 +0000867 menu.add_separator()
868 else:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000869 label, eventname = entry
David Scherer7aced172000-08-15 01:13:23 +0000870 checkbutton = (label[:1] == '!')
871 if checkbutton:
872 label = label[1:]
873 underline, label = prepstr(label)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000874 accelerator = get_accelerator(keydefs, eventname)
875 def command(text=text, eventname=eventname):
876 text.event_generate(eventname)
David Scherer7aced172000-08-15 01:13:23 +0000877 if checkbutton:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000878 var = self.get_var_obj(eventname, BooleanVar)
David Scherer7aced172000-08-15 01:13:23 +0000879 menu.add_checkbutton(label=label, underline=underline,
880 command=command, accelerator=accelerator,
881 variable=var)
882 else:
883 menu.add_command(label=label, underline=underline,
Kurt B. Kaiser84f48032002-09-26 22:13:22 +0000884 command=command,
885 accelerator=accelerator)
David Scherer7aced172000-08-15 01:13:23 +0000886
887 def getvar(self, name):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000888 var = self.get_var_obj(name)
David Scherer7aced172000-08-15 01:13:23 +0000889 if var:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000890 value = var.get()
891 return value
892 else:
893 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000894
895 def setvar(self, name, value, vartype=None):
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000896 var = self.get_var_obj(name, vartype)
David Scherer7aced172000-08-15 01:13:23 +0000897 if var:
898 var.set(value)
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000899 else:
900 raise NameError, name
David Scherer7aced172000-08-15 01:13:23 +0000901
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000902 def get_var_obj(self, name, vartype=None):
903 var = self.tkinter_vars.get(name)
David Scherer7aced172000-08-15 01:13:23 +0000904 if not var and vartype:
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +0000905 # create a Tkinter variable object with self.text as master:
906 self.tkinter_vars[name] = var = vartype(self.text)
David Scherer7aced172000-08-15 01:13:23 +0000907 return var
908
909 # Tk implementations of "virtual text methods" -- each platform
910 # reusing IDLE's support code needs to define these for its GUI's
911 # flavor of widget.
912
913 # Is character at text_index in a Python string? Return 0 for
914 # "guaranteed no", true for anything else. This info is expensive
915 # to compute ab initio, but is probably already known by the
916 # platform's colorizer.
917
918 def is_char_in_string(self, text_index):
919 if self.color:
920 # Return true iff colorizer hasn't (re)gotten this far
921 # yet, or the character is tagged as being in a string
922 return self.text.tag_prevrange("TODO", text_index) or \
923 "STRING" in self.text.tag_names(text_index)
924 else:
925 # The colorizer is missing: assume the worst
926 return 1
927
928 # If a selection is defined in the text widget, return (start,
929 # end) as Tkinter text indices, otherwise return (None, None)
930 def get_selection_indices(self):
931 try:
932 first = self.text.index("sel.first")
933 last = self.text.index("sel.last")
934 return first, last
935 except TclError:
936 return None, None
937
938 # Return the text widget's current view of what a tab stop means
939 # (equivalent width in spaces).
940
941 def get_tabwidth(self):
942 current = self.text['tabs'] or TK_TABWIDTH_DEFAULT
943 return int(current)
944
945 # Set the text widget's current view of what a tab stop means.
946
947 def set_tabwidth(self, newtabwidth):
948 text = self.text
949 if self.get_tabwidth() != newtabwidth:
950 pixels = text.tk.call("font", "measure", text["font"],
951 "-displayof", text.master,
Kurt B. Kaiserafdf71b2001-07-13 03:35:32 +0000952 "n" * newtabwidth)
David Scherer7aced172000-08-15 01:13:23 +0000953 text.configure(tabs=pixels)
954
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000955 # If ispythonsource and guess are true, guess a good value for
956 # indentwidth based on file content (if possible), and if
957 # indentwidth != tabwidth set usetabs false.
958 # In any case, adjust the Text widget's view of what a tab
959 # character means.
960
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000961 def set_indentation_params(self, ispythonsource, guess=True):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000962 if guess and ispythonsource:
963 i = self.guess_indent()
964 if 2 <= i <= 8:
965 self.indentwidth = i
966 if self.indentwidth != self.tabwidth:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +0000967 self.usetabs = False
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000968 self.set_tabwidth(self.tabwidth)
969
970 def smart_backspace_event(self, event):
971 text = self.text
972 first, last = self.get_selection_indices()
973 if first and last:
974 text.delete(first, last)
975 text.mark_set("insert", first)
976 return "break"
977 # Delete whitespace left, until hitting a real char or closest
978 # preceding virtual tab stop.
979 chars = text.get("insert linestart", "insert")
980 if chars == '':
981 if text.compare("insert", ">", "1.0"):
982 # easy: delete preceding newline
983 text.delete("insert-1c")
984 else:
985 text.bell() # at start of buffer
986 return "break"
987 if chars[-1] not in " \t":
988 # easy: delete preceding real char
989 text.delete("insert-1c")
990 return "break"
991 # Ick. It may require *inserting* spaces if we back up over a
992 # tab character! This is written to be clear, not fast.
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +0000993 tabwidth = self.tabwidth
994 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000995 assert have > 0
996 want = ((have - 1) // self.indentwidth) * self.indentwidth
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +0000997 # Debug prompt is multilined....
998 last_line_of_prompt = sys.ps1.split('\n')[-1]
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +0000999 ncharsdeleted = 0
1000 while 1:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001001 if chars == last_line_of_prompt:
Kurt B. Kaiser1bdca5e2002-12-16 22:25:10 +00001002 break
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001003 chars = chars[:-1]
1004 ncharsdeleted = ncharsdeleted + 1
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001005 have = len(chars.expandtabs(tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001006 if have <= want or chars[-1] not in " \t":
1007 break
1008 text.undo_block_start()
1009 text.delete("insert-%dc" % ncharsdeleted, "insert")
1010 if have < want:
1011 text.insert("insert", ' ' * (want - have))
1012 text.undo_block_stop()
1013 return "break"
1014
1015 def smart_indent_event(self, event):
1016 # if intraline selection:
1017 # delete it
1018 # elif multiline selection:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001019 # do indent-region
1020 # else:
1021 # indent one level
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001022 text = self.text
1023 first, last = self.get_selection_indices()
1024 text.undo_block_start()
1025 try:
1026 if first and last:
1027 if index2line(first) != index2line(last):
1028 return self.indent_region_event(event)
1029 text.delete(first, last)
1030 text.mark_set("insert", first)
1031 prefix = text.get("insert linestart", "insert")
1032 raw, effective = classifyws(prefix, self.tabwidth)
1033 if raw == len(prefix):
1034 # only whitespace to the left
1035 self.reindent_to(effective + self.indentwidth)
1036 else:
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001037 # tab to the next 'stop' within or to right of line's text:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001038 if self.usetabs:
1039 pad = '\t'
1040 else:
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001041 effective = len(prefix.expandtabs(self.tabwidth))
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001042 n = self.indentwidth
1043 pad = ' ' * (n - effective % n)
1044 text.insert("insert", pad)
1045 text.see("insert")
1046 return "break"
1047 finally:
1048 text.undo_block_stop()
1049
1050 def newline_and_indent_event(self, event):
1051 text = self.text
1052 first, last = self.get_selection_indices()
1053 text.undo_block_start()
1054 try:
1055 if first and last:
1056 text.delete(first, last)
1057 text.mark_set("insert", first)
1058 line = text.get("insert linestart", "insert")
1059 i, n = 0, len(line)
1060 while i < n and line[i] in " \t":
1061 i = i+1
1062 if i == n:
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001063 # the cursor is in or at leading indentation in a continuation
1064 # line; just inject an empty line at the start
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001065 text.insert("insert linestart", '\n')
1066 return "break"
1067 indent = line[:i]
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001068 # strip whitespace before insert point unless it's in the prompt
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001069 i = 0
Kurt B. Kaiser4ada7ad2002-12-29 22:03:38 +00001070 last_line_of_prompt = sys.ps1.split('\n')[-1]
1071 while line and line[-1] in " \t" and line != last_line_of_prompt:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001072 line = line[:-1]
1073 i = i+1
1074 if i:
1075 text.delete("insert - %d chars" % i, "insert")
1076 # strip whitespace after insert point
1077 while text.get("insert") in " \t":
1078 text.delete("insert")
1079 # start new line
1080 text.insert("insert", '\n')
1081
1082 # adjust indentation for continuations and block
1083 # open/close first need to find the last stmt
1084 lno = index2line(text.index('insert'))
1085 y = PyParse.Parser(self.indentwidth, self.tabwidth)
Kurt B. Kaiserb1754452005-11-18 22:05:48 +00001086 if not self.context_use_ps1:
1087 for context in self.num_context_lines:
1088 startat = max(lno - context, 1)
1089 startatindex = `startat` + ".0"
1090 rawtext = text.get(startatindex, "insert")
1091 y.set_str(rawtext)
1092 bod = y.find_good_parse_start(
1093 self.context_use_ps1,
1094 self._build_char_in_string_func(startatindex))
1095 if bod is not None or startat == 1:
1096 break
1097 y.set_lo(bod or 0)
1098 else:
1099 r = text.tag_prevrange("console", "insert")
1100 if r:
1101 startatindex = r[1]
1102 else:
1103 startatindex = "1.0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001104 rawtext = text.get(startatindex, "insert")
1105 y.set_str(rawtext)
Kurt B. Kaiserb1754452005-11-18 22:05:48 +00001106 y.set_lo(0)
1107
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001108 c = y.get_continuation_type()
1109 if c != PyParse.C_NONE:
1110 # The current stmt hasn't ended yet.
Kurt B. Kaiserb61602c2005-11-15 07:20:06 +00001111 if c == PyParse.C_STRING_FIRST_LINE:
1112 # after the first line of a string; do not indent at all
1113 pass
1114 elif c == PyParse.C_STRING_NEXT_LINES:
1115 # inside a string which started before this line;
1116 # just mimic the current indent
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001117 text.insert("insert", indent)
1118 elif c == PyParse.C_BRACKET:
1119 # line up with the first (if any) element of the
1120 # last open bracket structure; else indent one
1121 # level beyond the indent of the line with the
1122 # last open bracket
1123 self.reindent_to(y.compute_bracket_indent())
1124 elif c == PyParse.C_BACKSLASH:
1125 # if more than one line in this stmt already, just
1126 # mimic the current indent; else if initial line
1127 # has a start on an assignment stmt, indent to
1128 # beyond leftmost =; else to beyond first chunk of
1129 # non-whitespace on initial line
1130 if y.get_num_lines_in_stmt() > 1:
1131 text.insert("insert", indent)
1132 else:
1133 self.reindent_to(y.compute_backslash_indent())
1134 else:
Walter Dörwald70a6b492004-02-12 17:35:32 +00001135 assert 0, "bogus continuation type %r" % (c,)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001136 return "break"
1137
1138 # This line starts a brand new stmt; indent relative to
1139 # indentation of initial line of closest preceding
1140 # interesting stmt.
1141 indent = y.get_base_indent_string()
1142 text.insert("insert", indent)
1143 if y.is_block_opener():
1144 self.smart_indent_event(event)
1145 elif indent and y.is_block_closer():
1146 self.smart_backspace_event(event)
1147 return "break"
1148 finally:
1149 text.see("insert")
1150 text.undo_block_stop()
1151
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001152 # Our editwin provides a is_char_in_string function that works
1153 # with a Tk text index, but PyParse only knows about offsets into
1154 # a string. This builds a function for PyParse that accepts an
1155 # offset.
1156
1157 def _build_char_in_string_func(self, startindex):
1158 def inner(offset, _startindex=startindex,
1159 _icis=self.is_char_in_string):
1160 return _icis(_startindex + "+%dc" % offset)
1161 return inner
1162
1163 def indent_region_event(self, event):
1164 head, tail, chars, lines = self.get_region()
1165 for pos in range(len(lines)):
1166 line = lines[pos]
1167 if line:
1168 raw, effective = classifyws(line, self.tabwidth)
1169 effective = effective + self.indentwidth
1170 lines[pos] = self._make_blanks(effective) + line[raw:]
1171 self.set_region(head, tail, chars, lines)
1172 return "break"
1173
1174 def dedent_region_event(self, event):
1175 head, tail, chars, lines = self.get_region()
1176 for pos in range(len(lines)):
1177 line = lines[pos]
1178 if line:
1179 raw, effective = classifyws(line, self.tabwidth)
1180 effective = max(effective - self.indentwidth, 0)
1181 lines[pos] = self._make_blanks(effective) + line[raw:]
1182 self.set_region(head, tail, chars, lines)
1183 return "break"
1184
1185 def comment_region_event(self, event):
1186 head, tail, chars, lines = self.get_region()
1187 for pos in range(len(lines) - 1):
1188 line = lines[pos]
1189 lines[pos] = '##' + line
1190 self.set_region(head, tail, chars, lines)
1191
1192 def uncomment_region_event(self, event):
1193 head, tail, chars, lines = self.get_region()
1194 for pos in range(len(lines)):
1195 line = lines[pos]
1196 if not line:
1197 continue
1198 if line[:2] == '##':
1199 line = line[2:]
1200 elif line[:1] == '#':
1201 line = line[1:]
1202 lines[pos] = line
1203 self.set_region(head, tail, chars, lines)
1204
1205 def tabify_region_event(self, event):
1206 head, tail, chars, lines = self.get_region()
1207 tabwidth = self._asktabwidth()
1208 for pos in range(len(lines)):
1209 line = lines[pos]
1210 if line:
1211 raw, effective = classifyws(line, tabwidth)
1212 ntabs, nspaces = divmod(effective, tabwidth)
1213 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]
1214 self.set_region(head, tail, chars, lines)
1215
1216 def untabify_region_event(self, event):
1217 head, tail, chars, lines = self.get_region()
1218 tabwidth = self._asktabwidth()
1219 for pos in range(len(lines)):
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001220 lines[pos] = lines[pos].expandtabs(tabwidth)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001221 self.set_region(head, tail, chars, lines)
1222
1223 def toggle_tabs_event(self, event):
1224 if self.askyesno(
1225 "Toggle tabs",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001226 "Turn tabs " + ("on", "off")[self.usetabs] +
1227 "?\nIndent width " +
1228 ("will be", "remains at")[self.usetabs] + " 8.",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001229 parent=self.text):
1230 self.usetabs = not self.usetabs
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001231 # Try to prevent mixed tabs/spaces.
1232 # User must reset indent width manually after using tabs
1233 # if he insists on getting into trouble.
1234 self.indentwidth = 8
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001235 return "break"
1236
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001237 # XXX this isn't bound to anything -- see tabwidth comments
1238## def change_tabwidth_event(self, event):
1239## new = self._asktabwidth()
1240## if new != self.tabwidth:
1241## self.tabwidth = new
1242## self.set_indentation_params(0, guess=0)
1243## return "break"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001244
1245 def change_indentwidth_event(self, event):
1246 new = self.askinteger(
1247 "Indent width",
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001248 "New indent width (2-16)\n(Always use 8 when using tabs)",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001249 parent=self.text,
1250 initialvalue=self.indentwidth,
1251 minvalue=2,
1252 maxvalue=16)
Kurt B. Kaiser6af44982005-01-19 00:22:59 +00001253 if new and new != self.indentwidth and not self.usetabs:
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001254 self.indentwidth = new
1255 return "break"
1256
1257 def get_region(self):
1258 text = self.text
1259 first, last = self.get_selection_indices()
1260 if first and last:
1261 head = text.index(first + " linestart")
1262 tail = text.index(last + "-1c lineend +1c")
1263 else:
1264 head = text.index("insert linestart")
1265 tail = text.index("insert lineend +1c")
1266 chars = text.get(head, tail)
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001267 lines = chars.split("\n")
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001268 return head, tail, chars, lines
1269
1270 def set_region(self, head, tail, chars, lines):
1271 text = self.text
Kurt B. Kaiser1b3c2692002-09-15 21:31:30 +00001272 newchars = "\n".join(lines)
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001273 if newchars == chars:
1274 text.bell()
1275 return
1276 text.tag_remove("sel", "1.0", "end")
1277 text.mark_set("insert", head)
1278 text.undo_block_start()
1279 text.delete(head, tail)
1280 text.insert(head, newchars)
1281 text.undo_block_stop()
1282 text.tag_add("sel", head, "insert")
1283
1284 # Make string that displays as n leading blanks.
1285
1286 def _make_blanks(self, n):
1287 if self.usetabs:
1288 ntabs, nspaces = divmod(n, self.tabwidth)
1289 return '\t' * ntabs + ' ' * nspaces
1290 else:
1291 return ' ' * n
1292
1293 # Delete from beginning of line to insert point, then reinsert
1294 # column logical (meaning use tabs if appropriate) spaces.
1295
1296 def reindent_to(self, column):
1297 text = self.text
1298 text.undo_block_start()
1299 if text.compare("insert linestart", "!=", "insert"):
1300 text.delete("insert linestart", "insert")
1301 if column:
1302 text.insert("insert", self._make_blanks(column))
1303 text.undo_block_stop()
1304
1305 def _asktabwidth(self):
1306 return self.askinteger(
1307 "Tab width",
Kurt B. Kaiserca7329c2005-06-12 05:19:23 +00001308 "Columns per tab? (2-16)",
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001309 parent=self.text,
1310 initialvalue=self.indentwidth,
1311 minvalue=2,
1312 maxvalue=16) or self.tabwidth
1313
1314 # Guess indentwidth from text content.
1315 # Return guessed indentwidth. This should not be believed unless
1316 # it's in a reasonable range (e.g., it will be 0 if no indented
1317 # blocks are found).
1318
1319 def guess_indent(self):
1320 opener, indented = IndentSearcher(self.text, self.tabwidth).run()
1321 if opener and indented:
1322 raw, indentsmall = classifyws(opener, self.tabwidth)
1323 raw, indentlarge = classifyws(indented, self.tabwidth)
1324 else:
1325 indentsmall = indentlarge = 0
1326 return indentlarge - indentsmall
1327
1328# "line.col" -> line, as an int
1329def index2line(index):
1330 return int(float(index))
1331
1332# Look at the leading whitespace in s.
1333# Return pair (# of leading ws characters,
1334# effective # of leading blanks after expanding
1335# tabs to width tabwidth)
1336
1337def classifyws(s, tabwidth):
1338 raw = effective = 0
1339 for ch in s:
1340 if ch == ' ':
1341 raw = raw + 1
1342 effective = effective + 1
1343 elif ch == '\t':
1344 raw = raw + 1
1345 effective = (effective // tabwidth + 1) * tabwidth
1346 else:
1347 break
1348 return raw, effective
1349
1350import tokenize
1351_tokenize = tokenize
1352del tokenize
1353
Kurt B. Kaiserdcba6622004-12-21 22:10:32 +00001354class IndentSearcher(object):
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001355
1356 # .run() chews over the Text widget, looking for a block opener
1357 # and the stmt following it. Returns a pair,
1358 # (line containing block opener, line containing stmt)
1359 # Either or both may be None.
1360
1361 def __init__(self, text, tabwidth):
1362 self.text = text
1363 self.tabwidth = tabwidth
1364 self.i = self.finished = 0
1365 self.blkopenline = self.indentedline = None
1366
1367 def readline(self):
1368 if self.finished:
1369 return ""
1370 i = self.i = self.i + 1
Walter Dörwald70a6b492004-02-12 17:35:32 +00001371 mark = repr(i) + ".0"
Kurt B. Kaisercb7a3832002-09-14 02:34:23 +00001372 if self.text.compare(mark, ">=", "end"):
1373 return ""
1374 return self.text.get(mark, mark + " lineend+1c")
1375
1376 def tokeneater(self, type, token, start, end, line,
1377 INDENT=_tokenize.INDENT,
1378 NAME=_tokenize.NAME,
1379 OPENERS=('class', 'def', 'for', 'if', 'try', 'while')):
1380 if self.finished:
1381 pass
1382 elif type == NAME and token in OPENERS:
1383 self.blkopenline = line
1384 elif type == INDENT and self.blkopenline:
1385 self.indentedline = line
1386 self.finished = 1
1387
1388 def run(self):
1389 save_tabsize = _tokenize.tabsize
1390 _tokenize.tabsize = self.tabwidth
1391 try:
1392 try:
1393 _tokenize.tokenize(self.readline, self.tokeneater)
1394 except _tokenize.TokenError:
1395 # since we cut off the tokenizer early, we can trigger
1396 # spurious errors
1397 pass
1398 finally:
1399 _tokenize.tabsize = save_tabsize
1400 return self.blkopenline, self.indentedline
1401
1402### end autoindent code ###
1403
David Scherer7aced172000-08-15 01:13:23 +00001404def prepstr(s):
1405 # Helper to extract the underscore from a string, e.g.
1406 # prepstr("Co_py") returns (2, "Copy").
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001407 i = s.find('_')
David Scherer7aced172000-08-15 01:13:23 +00001408 if i >= 0:
1409 s = s[:i] + s[i+1:]
1410 return i, s
1411
1412
1413keynames = {
1414 'bracketleft': '[',
1415 'bracketright': ']',
1416 'slash': '/',
1417}
1418
Kurt B. Kaiser610c7e02004-04-24 03:01:48 +00001419def get_accelerator(keydefs, eventname):
1420 keylist = keydefs.get(eventname)
David Scherer7aced172000-08-15 01:13:23 +00001421 if not keylist:
1422 return ""
1423 s = keylist[0]
Kurt B. Kaiser220ecbc2002-09-16 02:13:15 +00001424 s = re.sub(r"-[a-z]\b", lambda m: m.group().upper(), s)
David Scherer7aced172000-08-15 01:13:23 +00001425 s = re.sub(r"\b\w+\b", lambda m: keynames.get(m.group(), m.group()), s)
1426 s = re.sub("Key-", "", s)
1427 s = re.sub("Cancel","Ctrl-Break",s) # dscherer@cmu.edu
1428 s = re.sub("Control-", "Ctrl-", s)
1429 s = re.sub("-", "+", s)
1430 s = re.sub("><", " ", s)
1431 s = re.sub("<", "", s)
1432 s = re.sub(">", "", s)
1433 return s
1434
1435
1436def fixwordbreaks(root):
1437 # Make sure that Tk's double-click and next/previous word
1438 # operations use our definition of a word (i.e. an identifier)
1439 tk = root.tk
1440 tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
1441 tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_]')
1442 tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_]')
1443
1444
1445def test():
1446 root = Tk()
1447 fixwordbreaks(root)
1448 root.withdraw()
1449 if sys.argv[1:]:
1450 filename = sys.argv[1]
1451 else:
1452 filename = None
1453 edit = EditorWindow(root=root, filename=filename)
1454 edit.set_close_hook(root.quit)
1455 root.mainloop()
1456 root.destroy()
1457
1458if __name__ == '__main__':
1459 test()