Files
wxWidgets/wxPython/wxPython/py/editor.py
Robin Dunn 5f933e58ef Patrick O'Brien's PyCrust package has been renamed to Py and now
includes several new tools.  As part of the change the location of the
pacakge has changed as well, it is now accessible as "from wxPython
import py" (or "from wx import py" using the new namespace.)  There
are still some transition moudules in the wxPython.lib.PyCrust mackage
that will issue a warning and then import what is needed from the new
package.  These will be removed in a future release.


git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/branches/WX_2_4_BRANCH@20104 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
2003-04-09 17:44:53 +00:00

734 lines
25 KiB
Python

"""PyAlaCarte and PyAlaMode editors."""
__author__ = "Patrick K. O'Brien <pobrien@orbtech.com>"
__cvsid__ = "$Id$"
__revision__ = "$Revision$"[11:-2]
from wxPython import wx
import base
import buffer
import crust
import dispatcher
import frame
import interpreter
import shell
import version
try:
True
except NameError:
True = 1==1
False = 1==0
class EditorFrame(frame.Frame):
"""Frame containing one editor."""
def __init__(self, parent=None, id=-1, title='PyAlaCarte',
pos=wx.wxDefaultPosition, size=(800, 600),
style=wx.wxDEFAULT_FRAME_STYLE, filename=None):
"""Create an EditorFrame instance."""
frame.Frame.__init__(self, parent, id, title, pos, size, style)
self._buffers = {}
self._buffer = None # Current buffer.
self.editor = None
self._statusText = title + ' - the tastiest Python editor.'
self.SetStatusText(self._statusText)
wx.EVT_IDLE(self, self.OnIdle)
self._setup()
if filename:
self.bufferCreate(filename)
def _setup(self):
"""Setup prior to first buffer creation.
Useful for subclasses."""
pass
def OnAbout(self, event):
"""Display an About window."""
title = 'About PyAlaCarte'
text = 'Another fine, flaky program.'
dialog = wx.wxMessageDialog(self, text, title,
wx.wxOK | wx.wxICON_INFORMATION)
dialog.ShowModal()
dialog.Destroy()
def OnClose(self, event):
"""Event handler for closing."""
for buffer in self._buffers.values():
self._buffer = buffer
if buffer.hasChanged():
cancel = self.bufferSuggestSave()
if cancel and event.CanVeto():
event.Veto()
return
self.Destroy()
def OnIdle(self, event):
"""Event handler for idle time."""
self._updateStatus()
self._updateTitle()
event.Skip()
def _updateStatus(self):
"""Show current status information."""
if self._buffer:
status = self._buffer.getStatus()
text = 'File: %s | Line: %d | Column: %d' % status
if text != self._statusText:
self.SetStatusText(text)
self._statusText = text
def _updateTitle(self):
"""Show current title information."""
title = self.GetTitle()
if self.bufferHasChanged():
if title.startswith('* '):
pass
else:
self.SetTitle('* ' + title)
else:
if title.startswith('* '):
self.SetTitle(title[2:])
def hasBuffer(self):
"""Return True if there is a current buffer."""
if self._buffer:
return True
else:
return False
def bufferClose(self):
"""Close buffer."""
if self.bufferHasChanged():
cancel = self.bufferSuggestSave()
if cancel:
return cancel
self.bufferDestroy()
cancel = False
return cancel
def bufferCreate(self, filename=None):
"""Create new buffer."""
self.bufferDestroy()
interp = interpreter.Interpreter(locals={})
self.editor = Editor(interp=interp, parent=self, filename=filename)
self._buffer = self.editor.buffer
self._buffers[self._buffer.id] = self._buffer
self._buffer.editor.SetFocus()
def bufferDestroy(self):
"""Destroy the current buffer."""
if self._buffer:
del self._buffers[self._buffer.id]
self._buffer = None
if self.editor:
self.editor.Destroy()
self.editor = None
def bufferHasChanged(self):
"""Return True if buffer has changed since last save."""
if self._buffer:
return self._buffer.hasChanged()
else:
return False
def bufferNew(self):
"""Create new buffer."""
if self.bufferHasChanged():
cancel = self.bufferSuggestSave()
if cancel:
return cancel
self.bufferCreate()
cancel = False
return cancel
def bufferOpen(self):
"""Open file in buffer."""
if self.bufferHasChanged():
cancel = self.bufferSuggestSave()
if cancel:
return cancel
filedir = ''
if self._buffer and self._buffer.doc.filedir:
filedir = self._buffer.doc.filedir
result = openSingle(directory=filedir)
if result.path:
self.bufferCreate(result.path)
cancel = False
return cancel
## def bufferPrint(self):
## """Print buffer."""
## pass
## def bufferRevert(self):
## """Revert buffer to version of file on disk."""
## pass
def bufferSave(self):
"""Save buffer to its file."""
if self._buffer.doc.filepath:
self._buffer.save()
cancel = False
else:
cancel = self.bufferSaveAs()
return cancel
def bufferSaveAs(self):
"""Save buffer to a new filename."""
if self.bufferHasChanged() and self._buffer.doc.filepath:
cancel = self.bufferSuggestSave()
if cancel:
return cancel
filedir = ''
if self._buffer and self._buffer.doc.filedir:
filedir = self._buffer.doc.filedir
result = saveSingle(directory=filedir)
if result.path:
self._buffer.saveAs(result.path)
cancel = False
else:
cancel = True
return cancel
def bufferSuggestSave(self):
"""Suggest saving changes. Return True if user selected Cancel."""
result = messageDialog(parent=None,
message='%s has changed.\n'
'Would you like to save it first'
'?' % self._buffer.name,
title='Save current file?')
if result.positive:
cancel = self.bufferSave()
else:
cancel = result.text == 'Cancel'
return cancel
def updateNamespace(self):
"""Update the buffer namespace for autocompletion and calltips."""
if self._buffer.updateNamespace():
self.SetStatusText('Namespace updated')
else:
self.SetStatusText('Error executing, unable to update namespace')
class EditorNotebookFrame(EditorFrame):
"""Frame containing one or more editors in a notebook."""
def __init__(self, parent=None, id=-1, title='PyAlaMode',
pos=wx.wxDefaultPosition, size=(800, 600),
style=wx.wxDEFAULT_FRAME_STYLE, filename=None):
"""Create an EditorNotebookFrame instance."""
EditorFrame.__init__(self, parent, id, title, pos,
size, style, filename)
def _setup(self):
"""Setup prior to first buffer creation.
Useful for subclasses."""
self._notebook = BufferNotebook(parent=self)
dispatcher.connect(receiver=self._bufferChange,
signal='BufferChange', sender=self._notebook)
intro = 'PyCrust %s' % version.VERSION
import imp
module = imp.new_module('__main__')
import __builtin__
module.__dict__['__builtins__'] = __builtin__
namespace = module.__dict__.copy()
self.crust = crust.Crust(parent=self._notebook, intro=intro, locals=namespace)
self.shell = self.crust.shell
# Override the filling so that status messages go to the status bar.
self.crust.filling.tree.setStatusText = self.SetStatusText
# Override the shell so that status messages go to the status bar.
self.shell.setStatusText = self.SetStatusText
# Fix a problem with the sash shrinking to nothing.
self.crust.filling.SetSashPosition(200)
self._notebook.AddPage(page=self.crust, text='PyCrust', select=True)
self._buffer = self.crust.buffer
self._buffers[self._buffer.id] = self._buffer
self._buffer.editor.SetFocus()
def _bufferChange(self, buffer):
"""Buffer change signal receiver."""
self._buffer = buffer
def OnAbout(self, event):
"""Display an About window."""
title = 'About PyAlaMode'
text = 'Another fine, flaky program.'
dialog = wx.wxMessageDialog(self, text, title,
wx.wxOK | wx.wxICON_INFORMATION)
dialog.ShowModal()
dialog.Destroy()
def _updateTitle(self):
"""Show current title information."""
title = self.GetTitle()
if self.bufferHasChanged():
if title.startswith('* '):
pass
else:
self.SetTitle('* ' + title)
else:
if title.startswith('* '):
self.SetTitle(title[2:])
def bufferCreate(self, filename=None):
"""Create new buffer."""
interp = interpreter.Interpreter(locals={})
editor = Editor(interp=interp, parent=self._notebook,
filename=filename)
self._buffer = editor.buffer
self._buffers[self._buffer.id] = self._buffer
self._notebook.AddPage(page=editor, text=self._buffer.name,
select=True)
self._buffer.editor.SetFocus()
def bufferDestroy(self):
"""Destroy the current buffer."""
selection = self._notebook.GetSelection()
## print "Destroy Selection:", selection
if selection > 0: # Don't destroy the PyCrust tab.
if self._buffer:
del self._buffers[self._buffer.id]
self._buffer = None # Do this before DeletePage().
self._notebook.DeletePage(selection)
def bufferNew(self):
"""Create new buffer."""
self.bufferCreate()
cancel = False
return cancel
def bufferOpen(self):
"""Open file in buffer."""
filedir = ''
if self._buffer and self._buffer.doc.filedir:
filedir = self._buffer.doc.filedir
result = openMultiple(directory=filedir)
for path in result.paths:
self.bufferCreate(path)
cancel = False
return cancel
class BufferNotebook(wx.wxNotebook):
"""A notebook containing a page for each buffer."""
def __init__(self, parent):
"""Create a BufferNotebook instance."""
wx.wxNotebook.__init__(self, parent, id=-1)
wx.EVT_NOTEBOOK_PAGE_CHANGING(self, self.GetId(), self.OnPageChanging)
wx.EVT_NOTEBOOK_PAGE_CHANGED(self, self.GetId(), self.OnPageChanged)
def OnPageChanging(self, event):
"""Page changing event handler."""
## old = event.GetOldSelection()
## print "Changing from old:", old
## new = event.GetOldSelection()
## print "Changing to new:", new
event.Skip()
def OnPageChanged(self, event):
"""Page changed event handler."""
## old = event.GetOldSelection()
## print "Changed from:", old
new = event.GetSelection()
## print "Changed to new:", new
page = self.GetPage(new)
buffer = page.buffer
buffer.editor.SetFocus()
dispatcher.send(signal='BufferChange', sender=self, buffer=buffer)
event.Skip()
class BufferEditorShellNotebookFrame(EditorFrame):
"""Frame containing one or more editor notebooks."""
def __init__(self, parent=None, id=-1, title='PyAlaMode',
pos=wx.wxDefaultPosition, size=(600, 400),
style=wx.wxDEFAULT_FRAME_STYLE,
filename=None, singlefile=False):
"""Create a BufferEditorShellNotebookFrame instance."""
self._singlefile = singlefile
EditorFrame.__init__(self, parent, id, title, pos,
size, style, filename)
def _setup(self):
"""Setup prior to first buffer creation.
Useful for subclasses."""
if not self._singlefile:
self._notebook = BufferNotebook(parent=self)
dispatcher.connect(receiver=self._bufferChange,
signal='BufferChange', sender=self._notebook)
def _bufferChange(self, buffer):
"""Buffer change signal receiver."""
self._buffer = buffer
def OnAbout(self, event):
"""Display an About window."""
title = 'About PyAlaMode'
text = 'Another fine, flaky program.'
dialog = wx.wxMessageDialog(self, text, title,
wx.wxOK | wx.wxICON_INFORMATION)
dialog.ShowModal()
dialog.Destroy()
def _updateTitle(self):
"""Show current title information."""
title = self.GetTitle()
if self.bufferHasChanged():
if title.startswith('* '):
pass
else:
self.SetTitle('* ' + title)
else:
if title.startswith('* '):
self.SetTitle(title[2:])
def bufferCreate(self, filename=None):
"""Create new buffer."""
if self._singlefile:
self.bufferDestroy()
notebook = self._notebook = EditorShellNotebook(parent=self,
filename=filename)
else:
notebook = EditorShellNotebook(parent=self._notebook,
filename=filename)
self._buffer = notebook.buffer
if not self._singlefile:
self._notebook.AddPage(page=notebook, text=self._buffer.name,
select=True)
self._buffers[self._buffer.id] = self._buffer
self._buffer.editor.SetFocus()
def bufferDestroy(self):
"""Destroy the current buffer."""
if self._buffer:
del self._buffers[self._buffer.id]
self._buffer = None # Do this before DeletePage().
if self._singlefile:
self._notebook.Destroy()
self._notebook = None
else:
selection = self._notebook.GetSelection()
print "Destroy Selection:", selection
self._notebook.DeletePage(selection)
def bufferNew(self):
"""Create new buffer."""
if self._singlefile and self.bufferHasChanged():
cancel = self.bufferSuggestSave()
if cancel:
return cancel
self.bufferCreate()
cancel = False
return cancel
def bufferOpen(self):
"""Open file in buffer."""
if self._singlefile and self.bufferHasChanged():
cancel = self.bufferSuggestSave()
if cancel:
return cancel
filedir = ''
if self._buffer and self._buffer.doc.filedir:
filedir = self._buffer.doc.filedir
if self._singlefile:
result = openSingle(directory=filedir)
if result.path:
self.bufferCreate(result.path)
else:
result = openMultiple(directory=filedir)
for path in result.paths:
self.bufferCreate(path)
cancel = False
return cancel
class BufferEditorShellNotebook(wx.wxNotebook):
"""A notebook containing a page for each buffer."""
def __init__(self, parent):
"""Create a BufferEditorShellNotebook instance."""
wx.wxNotebook.__init__(self, parent, id=-1)
wx.EVT_NOTEBOOK_PAGE_CHANGED(self, self.GetId(), self.OnPageChanged)
def OnPageChanged(self, event):
"""Page changed event handler."""
## old = event.GetOldSelection()
## print "Changed from old:", old
new = event.GetSelection()
## print "Changed to new:", new
page = self.GetPage(new)
buffer = page.buffer
subselection = page.GetSelection()
page.focus(subselection)
dispatcher.send(signal='BufferChange', sender=self, buffer=buffer)
event.Skip()
class EditorShellNotebook(wx.wxNotebook):
"""A notebook containing an editor page and a shell page."""
def __init__(self, parent, filename=None):
"""Create an EditorShellNotebook instance."""
wx.wxNotebook.__init__(self, parent, id=-1)
usePanels = True
if usePanels:
shellparent = shellpanel = wx.wxPanel(self, -1)
editorparent = editorpanel = wx.wxPanel(self, -1)
else:
shellparent = self
editorparent = self
self.shell = shell.Shell(parent=shellparent,
style=wx.wxCLIP_CHILDREN | wx.wxSUNKEN_BORDER)
self.editor = Editor(interp=self.shell.interp, parent=editorparent,
filename=filename)
if usePanels:
self.AddPage(page=editorpanel, text='File', select=True)
self.AddPage(page=shellpanel, text='Shell')
# Setup sizers
shellsizer = wx.wxBoxSizer(wx.wxVERTICAL)
shellsizer.Add(self.shell, 1, wx.wxEXPAND)
shellpanel.SetSizer(shellsizer)
shellpanel.SetAutoLayout(True)
editorsizer = wx.wxBoxSizer(wx.wxVERTICAL)
editorsizer.Add(self.editor, 1, wx.wxEXPAND)
editorpanel.SetSizer(editorsizer)
editorpanel.SetAutoLayout(True)
else:
self.AddPage(page=self.editor, text='File', select=True)
self.AddPage(page=self.shell, text='Shell')
self.buffer = self.editor.buffer
self.editor.SetFocus()
wx.EVT_NOTEBOOK_PAGE_CHANGED(self, self.GetId(), self.OnPageChanged)
def OnPageChanged(self, event):
"""Page changed event handler."""
selection = event.GetSelection()
self.focus(selection)
event.Skip()
def focus(self, selection):
if selection == 0:
self.editor.SetFocus()
else:
self.shell.SetFocus()
class Editor(base.Editor):
"""Editor based on StyledTextCtrl."""
def __init__(self, interp, parent, id=-1, pos=wx.wxDefaultPosition,
size=wx.wxDefaultSize,
style=wx.wxCLIP_CHILDREN | wx.wxSUNKEN_BORDER,
filename=None):
"""Create a Editor instance."""
base.Editor.__init__(self, parent, id, pos, size, style)
self.interp = interp
# Find out for which keycodes the interpreter will autocomplete.
self.autoCompleteKeys = self.interp.getAutoCompleteKeys()
# Assign handlers for keyboard events.
wx.EVT_CHAR(self, self.OnChar)
wx.EVT_KEY_DOWN(self, self.OnKeyDown)
self.buffer = buffer.Buffer(editor=self, interp=self.interp,
filename=filename)
def OnChar(self, event):
"""Keypress event handler.
Only receives an event if OnKeyDown calls event.Skip() for the
corresponding event."""
key = event.KeyCode()
if key in self.autoCompleteKeys:
# Usually the dot (period) key activates auto completion.
if self.AutoCompActive():
self.AutoCompCancel()
self.ReplaceSelection('')
self.AddText(chr(key))
text, pos = self.GetCurLine()
text = text[:pos]
if self.autoComplete:
self.autoCompleteShow(text)
elif key == ord('('):
# The left paren activates a call tip and cancels an
# active auto completion.
if self.AutoCompActive():
self.AutoCompCancel()
self.ReplaceSelection('')
self.AddText('(')
text, pos = self.GetCurLine()
text = text[:pos]
self.autoCallTipShow(text)
else:
# Allow the normal event handling to take place.
event.Skip()
def OnKeyDown(self, event):
"""Key down event handler."""
key = event.KeyCode()
# If the auto-complete window is up let it do its thing.
if self.AutoCompActive():
event.Skip()
return
controlDown = event.ControlDown()
altDown = event.AltDown()
shiftDown = event.ShiftDown()
# Let Ctrl-Alt-* get handled normally.
if controlDown and altDown:
event.Skip()
# Increase font size.
elif controlDown and key in (ord(']'),):
dispatcher.send(signal='FontIncrease')
# Decrease font size.
elif controlDown and key in (ord('['),):
dispatcher.send(signal='FontDecrease')
# Default font size.
elif controlDown and key in (ord('='),):
dispatcher.send(signal='FontDefault')
else:
event.Skip()
def autoCompleteShow(self, command):
"""Display auto-completion popup list."""
list = self.interp.getAutoCompleteList(command,
includeMagic=self.autoCompleteIncludeMagic,
includeSingle=self.autoCompleteIncludeSingle,
includeDouble=self.autoCompleteIncludeDouble)
if list and len(list) < 2000:
options = ' '.join(list)
offset = 0
self.AutoCompShow(offset, options)
def autoCallTipShow(self, command):
"""Display argument spec and docstring in a popup window."""
if self.CallTipActive():
self.CallTipCancel()
(name, argspec, tip) = self.interp.getCallTip(command)
if tip:
dispatcher.send(signal='Shell.calltip', sender=self, calltip=tip)
if not self.autoCallTip:
return
if argspec:
startpos = self.GetCurrentPos()
self.AddText(argspec + ')')
endpos = self.GetCurrentPos()
self.SetSelection(endpos, startpos)
if tip:
curpos = self.GetCurrentPos()
size = len(name)
tippos = curpos - (size + 1)
fallback = curpos - self.GetColumn(curpos)
# In case there isn't enough room, only go back to the
# fallback.
tippos = max(tippos, fallback)
self.CallTipShow(tippos, tip)
self.CallTipSetHighlight(0, size)
class DialogResults:
"""DialogResults class."""
def __init__(self, returned):
"""Create a wrapper for the results returned by a dialog."""
self.returned = returned
self.positive = returned in (wx.wxID_OK, wx.wxID_YES)
self.text = self._asString()
def __repr__(self):
return str(self.__dict__)
def _asString(self):
returned = self.returned
if returned == wx.wxID_OK:
return "Ok"
elif returned == wx.wxID_CANCEL:
return "Cancel"
elif returned == wx.wxID_YES:
return "Yes"
elif returned == wx.wxID_NO:
return "No"
def fileDialog(parent=None, title='Open', directory='', filename='',
wildcard='All Files (*.*)|*.*',
style=wx.wxOPEN | wx.wxMULTIPLE):
"""File dialog wrapper function."""
dialog = wx.wxFileDialog(parent, title, directory, filename,
wildcard, style)
result = DialogResults(dialog.ShowModal())
if result.positive:
result.paths = dialog.GetPaths()
else:
result.paths = []
dialog.Destroy()
return result
def openSingle(parent=None, title='Open', directory='', filename='',
wildcard='All Files (*.*)|*.*', style=wx.wxOPEN):
"""File dialog wrapper function."""
dialog = wx.wxFileDialog(parent, title, directory, filename,
wildcard, style)
result = DialogResults(dialog.ShowModal())
if result.positive:
result.path = dialog.GetPath()
else:
result.path = None
dialog.Destroy()
return result
def openMultiple(parent=None, title='Open', directory='', filename='',
wildcard='All Files (*.*)|*.*',
style=wx.wxOPEN | wx.wxMULTIPLE):
"""File dialog wrapper function."""
return fileDialog(parent, title, directory, filename, wildcard, style)
def saveSingle(parent=None, title='Save', directory='', filename='',
wildcard='All Files (*.*)|*.*',
style=wx.wxSAVE | wx.wxHIDE_READONLY | wx.wxOVERWRITE_PROMPT):
"""File dialog wrapper function."""
dialog = wx.wxFileDialog(parent, title, directory, filename,
wildcard, style)
result = DialogResults(dialog.ShowModal())
if result.positive:
result.path = dialog.GetPath()
else:
result.path = None
dialog.Destroy()
return result
def directory(parent=None, message='Choose a directory', path='', style=0,
pos=wx.wxDefaultPosition, size=wx.wxDefaultSize):
"""Dir dialog wrapper function."""
dialog = wx.wxDirDialog(parent, message, path, style, pos, size)
result = DialogResults(dialog.ShowModal())
if result.positive:
result.path = dialog.GetPath()
else:
result.path = None
dialog.Destroy()
return result
def messageDialog(parent=None, message='', title='Message box',
style=wx.wxYES_NO | wx.wxCANCEL | wx.wxCENTRE | wx.wxICON_QUESTION,
pos=wx.wxDefaultPosition):
"""Message dialog wrapper function."""
dialog = wx.wxMessageDialog(parent, message, title, style, pos)
result = DialogResults(dialog.ShowModal())
dialog.Destroy()
return result