root/trunk/ide/Editor.py

Revision 4655, 32.3 kB (checked in by paul, 4 weeks ago)

Added the "Jump-to-line" focus fix from [4654] to trunk.

  • Property svn:eol-style set to native
  • Property svn:executable set to
Line 
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import sys
5 import os
6 import os.path
7 import keyword
8 import code
9 import inspect
10 import operator
11 import dabo
12 import dabo.dEvents as dEvents
13 from dabo.dLocalize import _
14 _Use_Subprocess = True
15 try:
16     import subprocess
17 except ImportError:
18     _Use_Subprocess = False
19 from dabo.lib.reportUtils import getTempFile
20 if __name__ == "__main__":
21     dabo.ui.loadUI("wx")
22
23
24 class EditPageSplitter(dabo.ui.dSplitter):
25     def __init__(self, *args, **kwargs):
26         kwargs["createPanes"] = True
27         super(EditPageSplitter, self).__init__(*args, **kwargs)
28         self.ShowPanelSplitMenu = False
29
30     def initProperties(self):
31         self.Width = 250
32         self.Height = 200
33         self.MinimumPanelSize = 20
34
35     def onSashDoubleClick(self, evt):
36         evt.stop()
37        
38     def onContextMenu(self, evt):
39         evt.stop()
40        
41     def onSashPositionChanged(self, evt):
42         self.Parent.updateSashPos()
43
44
45
46 class EditorEditor(dabo.ui.dEditor):
47     def addDroppedText(self, txt):
48         curr = self.Value
49         ss, se = self.SelectionStart, self.SelectionEnd
50         self.Value = "%s%s%s" % (curr[:ss], txt, curr[se:])
51         self.SelectionStart = ss
52         self.SelectionEnd = ss + len(txt)
53        
54    
55     def _getTextSource(self):
56         return self.Form.getTextSource()
57
58
59
60 class EditorPage(dabo.ui.dPage):
61     def initProperties(self):
62         self.p = None
63         self.outputText = ""
64         self._outputSashExtra = self.Application.getUserSetting("editorform.outputSashExtra", 100)
65
66
67     def afterInit(self):
68         self.splitter = EditPageSplitter(self, Orientation="h")
69         self.splitter.SashPosition = self.Height - self._outputSashExtra
70        
71         self.splitter.Panel1.Sizer = dabo.ui.dSizer()
72         self.splitter.Panel2.Sizer = dabo.ui.dSizer()
73    
74         self.editor = EditorEditor(self.splitter.Panel1)
75         self.editor.UseBookmarks = True
76         self.editor.page = self
77         self.output = dabo.ui.dEditBox(self.splitter.Panel2)
78         self.output.ReadOnly = True
79        
80         self.splitter.Panel1.Sizer.append1x(self.editor)
81         self.splitter.Panel2.Sizer.append1x(self.output)
82        
83         self.Sizer = dabo.ui.dSizer()
84         self.Sizer.append1x(self.splitter)
85                
86         self.updateTimer = dabo.ui.callEvery(1000, self.outputUpdate)
87        
88         self.layout()
89        
90         self.editor.setFocus()
91         self.editor.bindEvent(dEvents.TitleChanged, self.onTitleChanged)
92         self.editor.bindEvent(dEvents.MouseRightClick, self.Form.onEditorRightClick)
93         # Set up the file drop target
94         self.editor.DroppedFileHandler = self.Form
95         # Set up the text drop target
96         ### NOTE: currently we can only have text or file drops, not both
97         #self.editor.DroppedTextHandler = self.Form
98         # Update Hide/Show of output. Default to hidden
99         self.showOutput(self.Application.getUserSetting("visibleOutput", False))
100         # Set the initial title
101         dabo.ui.callAfter(self.onTitleChanged, None)
102         dabo.ui.callAfter(self.splitter._setOrientation, "h") #Its weird without this, self.splitter._constructed is not true
103         dabo.ui.callAfter(self.updateSashPos)
104
105
106     def onResize(self, evt):
107         self.splitter.SashPosition = self.Height - self._outputSashExtra
108
109        
110     def updateSashPos(self):
111         if self.splitter.SashPosition > 0:
112             self._outputSashExtra = self.Height - self.splitter.SashPosition
113         self.Application.setUserSetting("editorform.outputSashExtra", self._outputSashExtra)
114        
115    
116     def outputUpdate(self):
117         if self and self.p:
118             #need a nonblocking way of getting stdout and stderr
119             self.outputText = self.outputText + self.p.stdout.read() + self.p.stderr.read()
120             if not self.p.poll() is None:
121                 self.p = None
122             self.output.Value = self.outputText
123    
124    
125     def showOutput(self, show):
126         self.splitter.Split = show
127    
128    
129     def onTitleChanged(self, evt):
130         title = self.editor._title
131         self.Caption = title
132         self.Form.onTitleChanged(evt)
133    
134    
135     def onPageEnter(self, evt):
136         self.editor.setFocus()
137    
138    
139     def onPageLeave(self, evt):
140         self.editor.setInactive()
141    
142    
143     def onDestroy(self, evt):
144         dabo.ui.callAfter(self.Form.onTitleChanged, evt)
145
146
147     def _getPathInfo(self):
148         try:
149             ret = self.editor._fileName
150         except:
151             ret = ""
152         return ret
153
154
155     PathInfo = property(_getPathInfo, None, None,
156             _("Path to the file being edited  (str)") )
157            
158        
159
160 class EditorPageFrame(dabo.ui.dPageFrame):
161     def beforeInit(self):
162         self.PageClass = EditorPage
163    
164    
165     def getTextSource(self):
166         txt = []
167         for pg in self.Pages:
168             txt.append(pg.editor.Value)
169         return " ".join(txt)
170
171
172     def checkChanges(self, closing=False):
173         """Cycle through the pages, and if any contain unsaved changes,
174         ask the user if they should be saved. If they cancel, immediately
175         stop and return False. If they say yes, save the contents. Unless
176         they cancel, close each page.
177         """
178         ret = True
179         for pg in self.Pages[::-1]:
180             ed = pg.editor
181             ret = ed.checkChangesAndContinue()
182             if not ret:
183                 break
184             elif closing:
185                 self.PageCount -= 1
186         return ret
187    
188    
189     def findEditor(self, pth, selectIt=False):
190         """Returns the editor that is editing the specified file. If there
191         is no matching editor, returns None. If selectIt is True, makes that
192         editor the active editor.
193         """
194         ret = None
195         for pg in self.Pages:
196             if pg.editor._fileName == pth:
197                 ret = pg.editor
198                 if selectIt:
199                     self.SelectedPage = pg
200         return ret
201
202        
203     def editFile(self, pth, selectIt=False):
204         ret = self.findEditor(pth, selectIt)
205         if ret is None:
206             # Create a new page
207             pg = self.getBlankPage(True)
208             try:
209                 fileTarget = pg.editor.openFile(pth)
210                 if fileTarget:
211                     self.SelectedPage = pg
212                     ret = pg
213             except:
214                 dabo.ui.callAfter(self.removePage, pg)
215                 ret = None
216         return ret
217            
218            
219     def getBlankPage(self, create=False):
220         """Returns the first page that is not associated with a file,
221         and which has not been modified. If no such page exists, and
222         the 'create' parameter is True, a new blank page is created
223         and returned. Otherwise, returns None.
224         """
225         ret = None
226         for pg in self.Pages:
227             ed = pg.editor
228             if ed._title.strip() == ed._newFileName.strip():
229                 ret = pg
230                 break
231         if ret is None and create:
232             self.PageCount += 1
233             ret = self.Pages[-1]
234         return ret
235        
236    
237     def closeEditor(self, ed=None, checkChanges=True):
238         if ed is None:
239             ed = self.SelectedPage.editor
240         if checkChanges:
241             ret = ed.checkChangesAndContinue()
242         else:
243             ret = True
244         if ret is not False:
245             self.removePage(ed.page)
246         return ret
247        
248            
249     def newEditor(self):
250         ret = self.getBlankPage(True)
251         self.SelectedPage = ret
252         return ret
253
254
255     def selectByCaption(self, cap):
256         pgs = [pg for pg in self.Pages
257                 if pg.Caption == cap]
258         if pgs:
259             pg = pgs[0]
260         else:
261             dabo.errorLog.write(_("No matching page for %s") % cap)
262             return
263         self.SelectedPage = pg
264         pg.editor.setFocus()
265    
266    
267     def edFocus(self):
268         self.SelectedPage.editor.setFocus()
269    
270        
271     def _getCurrentEditor(self):
272         try:
273             return self.SelectedPage.editor
274         except:
275             return None
276            
277            
278     def _getTitle(self):
279         sp = self.SelectedPage
280         try:
281             ret = sp.PathInfo
282             if sp.editor.Modified:
283                 ret += " *"
284         except:
285             ret = ""
286         return ret
287
288    
289     CurrentEditor = property(_getCurrentEditor, None, None,
290             _("References the currently active editor  (dEditor)"))
291
292     Title = property(_getTitle, None, None,
293             _("Title of the active page  (str)") )
294            
295            
296
297 class EditorForm(dabo.ui.dForm):
298     def __init__(self, *args, **kwargs):
299         super(EditorForm, self).__init__(*args, **kwargs)
300        
301        
302     def afterInit(self):
303         # Set up the file drop target
304         self.DroppedFileHandler = self
305         pnl = dabo.ui.dPanel(self)
306         self.Sizer.append1x(pnl)
307         pnl.Sizer = dabo.ui.dSizer("v")
308         self._lastPath = self.Application.getUserSetting("lastPath", os.getcwd())
309         super(EditorForm, self).afterInit()
310         self.Caption = _("Dabo Editor")
311         self.funcButton = dabo.ui.dImage(pnl, ScaleMode="Clip", Size=(22,22))
312         self.funcButton.Picture = dabo.ui.imageFromData(funcButtonData())
313         self.funcButton.bindEvent(dEvents.MouseLeftDown, self.onFuncButton)
314         self.bmkButton = dabo.ui.dImage(pnl, ScaleMode="Clip", Size=(22,22))
315         self.bmkButton.Picture = dabo.ui.imageFromData(bmkButtonData())
316         self.bmkButton.bindEvent(dEvents.MouseLeftDown, self.onBmkButton)
317
318         self.prntButton = dabo.ui.dBitmapButton(pnl, Size=(22,22))
319         self.prntButton.Picture = "print"
320         self.prntButton.bindEvent(dEvents.Hit, self.onPrint)
321        
322         self.lexSelector = dabo.ui.dDropdownList(pnl, ValueMode="String")
323         self.lexSelector.bindEvent(dEvents.Hit, self.onLexSelect)
324
325         btnSizer = dabo.ui.dSizer("H", DefaultSpacing=4)
326         btnSizer.append(self.funcButton)
327         btnSizer.append(self.bmkButton)
328         btnSizer.append(self.prntButton)
329         btnSizer.appendSpacer(10, proportion=1)
330         lbl = dabo.ui.dLabel(pnl, Caption=_("Language:"))
331         if not self.Application.Platform.lower() == "win":
332             lbl.FontSize -= 2
333             self.lexSelector.FontSize -= 2
334         btnSizer.append(lbl, valign="middle")
335         btnSizer.append(self.lexSelector, valign="middle")
336
337         pnl.Sizer.append(btnSizer, "x", border=4)       
338        
339         self.pgfEditor = EditorPageFrame(pnl, TabPosition="Top")
340         self.pgfEditor.bindEvent(dEvents.PageChanged, self.onEditorPageChanged)
341         pnl.Sizer.append1x(self.pgfEditor)
342         self.layout()
343         self.fillMenu()
344         dabo.ui.callAfter(self.showPage, 0)
345
346
347     def showPage(self, pg):
348         """Shows the specified page, if it exists."""
349         try:
350             self.pgfEditor.SelectedPage = pg
351             self.pgfEditor.edFocus()
352         except:
353             pass
354
355
356     def onPrint(self, evt):
357         self.CurrentEditor.onPrint()
358
359
360     def onActivate(self, evt):
361         """Check the files to see if any have been updated on disk."""
362         self.checkForUpdatedFiles()
363
364
365     def checkForUpdatedFiles(self):
366         """If any file being edited has not been modified, and there is a more recent version
367         on disk, update the file with the version on disk.
368         """
369         for pg in self.pgfEditor.Pages:
370             ed = pg.editor
371             if not ed.isChanged() and ed.checkForDiskUpdate():
372                 selpos = ed.SelectionPosition
373                 ed.openFile(ed._fileName)
374                 ed.SelectionPosition = selpos
375
376
377     def onLexSelect(self, evt):
378         self.CurrentEditor.Language = self.lexSelector.Value
379
380        
381     def onFuncButton(self, evt):
382         evt.stop()
383         flist = self.CurrentEditor.getFunctionList()
384         pop = dabo.ui.dMenu()
385         for nm, pos, iscls in flist:
386             prompt = nm
387             if not iscls:
388                 prompt = " - %s" % nm
389             itm = pop.append(prompt, OnHit=self.onFunctionPop)
390             itm.textPosition = pos
391         self.showContextMenu(pop)
392         del pop
393    
394    
395     def onFunctionPop(self, evt):
396         ed = self.CurrentEditor
397         pos = evt.menuItem.textPosition
398         currLine = ed.LineNumber
399         newLine = ed.getLineFromPosition(pos)
400         if newLine > currLine:
401             ed.moveToEnd()
402         ed.ensureLineVisible(newLine)
403         ed.LineNumber = newLine
404         nextLinePos = ed.getPositionFromLine(newLine+1)
405         ed.SelectionPosition = (pos, nextLinePos-1)
406    
407    
408     def onIdle(self, evt):
409         ed = self.CurrentEditor
410         if ed:
411             self.StatusText = "Line: %s, Col: %s" % (ed.LineNumber, ed.Column)
412    
413    
414     def getTextSource(self):
415         return self.pgfEditor.getTextSource()
416
417
418     def onEditorRightClick(self, evt):
419         ed = self.CurrentEditor
420         pos = evt.mousePosition
421         pp = ed.getPositionFromXY(pos)
422         if pp < 0:
423             # They clicked outside of the text area of the line. Find
424             # a position just to the right of the margin
425             lpos = ed.getMarginWidth()
426             pp = ed.getPositionFromXY(lpos, pos[1])
427         if pp < 0:
428             # This is a totally blank line. Nothing can be done
429             return
430         ln = ed.getLineFromPosition(pp)
431         ed.LineNumber = ln
432         self.onBmkButton(evt)
433        
434                
435     def onBmkButton(self, evt):
436         evt.stop()
437         ed = self.CurrentEditor
438         bmkList = ed.getBookmarkList()
439         pop = dabo.ui.dMenu()
440         currBmk = ed.getCurrentLineBookmark()
441         if not currBmk:
442             pop.append(_("Set Bookmark..."), OnHit=self.onSetBmk)
443         else:
444             pop.append(_("Clear Bookmark..."), OnHit=self.onClearBmk)
445         if bmkList:
446             pop.append(_("Clear All Bookmarks"), OnHit=self.onClearAllBmk)
447             pop.appendSeparator()
448             for nm in bmkList:
449                 itm = pop.append(nm, OnHit=self.onBookmarkPop)
450         self.showContextMenu(pop)
451         del pop
452    
453    
454     def onBookmarkPop(self, evt):
455         """Navigate to the chosen bookmark."""
456         self.CurrentEditor.findBookmark(evt.prompt)
457    
458    
459     def onSetBmk(self, evt):
460         """Need to ask the user for a name for this bookmark."""
461         nm = dabo.ui.getString(message=_("Name for this bookmark:"),
462                 caption=_("New Bookmark"))
463         if not nm:
464             # User canceled
465             return
466         ed = self.CurrentEditor
467         if nm in ed.getBookmarkList():
468             msg = _("There is already a bookmark named '%s'. Creating a new bookmark "
469                     "with the same name will delete the old one. Are you sure you want "
470                     "to do this?") % nm
471             if not dabo.ui.areYouSure(message=msg, title=_("Duplicate Name"),
472                     defaultNo=True, cancelButton=False):
473                 return
474         self.CurrentEditor.setBookmark(nm)
475        
476
477     def onClearBmk(self, evt):
478         """Clear the current bookmark."""
479         ed = self.CurrentEditor
480         bmk = ed.getCurrentLineBookmark()
481         if bmk:
482             ed.clearBookmark(bmk)
483    
484    
485     def onClearAllBmk(self, evt):
486         """Remove all the bookmarks."""
487         self.CurrentEditor.clearAllBookmarks()
488        
489
490     def onEditorPageChanged(self, evt):
491         self.checkForUpdatedFiles()
492         self.onTitleChanged(evt)
493         self.setCheckedMenus()
494         self.updateLex()
495    
496    
497     def updateLex(self):
498         if self.CurrentEditor:
499             if not self.lexSelector.Choices:
500                 self.lexSelector.Choices = self.CurrentEditor.getAvailableLanguages()
501             self.lexSelector.Value = self.CurrentEditor.Language
502    
503    
504     def setCheckedMenus(self):
505         ed = self.CurrentEditor
506         if ed is None:
507             self._autoAutoItem.Checked = self._wrapItem.Checked = self._synColorItem.Checked = False
508         else:
509             self._autoAutoItem.Checked = ed.AutoAutoComplete
510             self._wrapItem.Checked = ed.WordWrap
511             self._synColorItem.Checked = ed.SyntaxColoring
512         self._showOutItem.Checked = self.Application.getUserSetting("visibleOutput", False)
513        
514        
515     def beforeClose(self, evt):
516         ret= self.pgfEditor.checkChanges(closing=True)
517         return ret
518
519
520     def processDroppedFiles(self, filelist):
521         """Try to open each file up in an editor tab."""
522         self.openRecursively(filelist)
523
524
525     def processDroppedText(self, txt):
526         """Add the text to the current editor."""
527         self.CurrentEditor.addDroppedText(txt)
528    
529    
530     def openRecursively(self, filelist):
531         if isinstance(filelist, basestring):
532             # Individual file passed
533             filelist = [filelist]
534         for ff in filelist:
535             if os.path.isdir(ff):
536                 for fname in os.listdir(ff):
537                     self.openRecursively(os.path.join(ff, fname))
538             else:
539                 if os.path.isfile(ff):
540                     self.openFile(ff, justReportErrors=True)
541
542
543     def onDocumentationHint(self, evt):
544         # Eventually, a separate IDE window can optionally display help contents
545         # for the object. For now, just print the longdoc to the infolog.
546         dabo.infoLog.write(_("Documentation Hint received:\n\n%s") % evt.EventData["longDoc"])
547
548
549     def onTitleChanged(self, evt):
550         if self and self.pgfEditor:
551             self.Caption = _("Dabo Editor: %s") % self.pgfEditor.Title