data in Python source code, and the generated code now uses streams to
 convert the image data to wxImage, wxBitmap, or wxIcon.
 
+Added wxPython.lib.rcsizer which contains RowColSizer.  This sizer is
+based on code from Niki Spahiev and lets you specify a row and column
+for each item, as well as optional column or row spanning.  Cells with
+not item assigned to it are just left blank.  Stretchable rows or
+columns are specified and work the same as in wxFlexGridSizer.
+
 
 
 2.3.2.1
 
 _treeList = [
     ('New since last release', ['wxGenericDirCtrl',
                                 'wxImageFromStream',
+                                'RowColSizer',
                                 ]),
 
     ('Windows', ['wxFrame', 'wxDialog', 'wxMiniFrame',
                   'wxEditableListBox', 'wxLEDNumberCtrl',
                   ]),
 
-    ('Window Layout', ['wxLayoutConstraints', 'LayoutAnchors', 'Sizers', 'XML_Resource']),
+    ('Window Layout', ['wxLayoutConstraints', 'LayoutAnchors', 'Sizers', 'XML_Resource',
+                       'RowColSizer',
+                       ]),
 
     ('Miscellaneous', [ 'DragAndDrop', 'CustomDragAndDrop', 'URLDragAndDrop',
                         'FontEnumerator',
 
--- /dev/null
+
+from wxPython.wx import *
+from wxPython.lib.rcsizer import RowColSizer
+
+
+#----------------------------------------------------------------------
+
+class TestPanel(wxPanel):
+    def __init__(self, parent):
+        wxPanel.__init__(self, parent, -1)
+
+        sizer = RowColSizer()
+        text = "This sizer lays out it's items by row and column that are"\
+               "specified explicitly when the item is added to the sizer.\n"\
+               "Grid cells with nothing in them are supported and column-"\
+               "or row-spanning is handled as well.  Growable rows and\n"\
+               "columns are specified just like the wxFlexGridSizer."
+        sizer.Add(wxStaticText(self, -1, text), row=1, col=1, colspan=5)
+
+        sizer.Add(wxTextCtrl(self, -1, "(3,1)"), flag=wxEXPAND, row=3, col=1)
+        sizer.Add(wxTextCtrl(self, -1, "(3,2)"), row=3, col=2)
+        sizer.Add(wxTextCtrl(self, -1, "(3,3)"), row=3, col=3)
+        sizer.Add(wxTextCtrl(self, -1, "(3,4)"), row=3, col=4)
+        sizer.Add(wxTextCtrl(self, -1, "(4,2) span:(2,2)"), flag=wxEXPAND,
+                  row=4, col=2, rowspan=2, colspan=2)
+        sizer.Add(wxTextCtrl(self, -1, "(6,4)"), row=6, col=4)
+        sizer.Add(wxTextCtrl(self, -1, "(7,2)"), row=7, col=2)
+        sizer.Add(wxTextCtrl(self, -1, "(8,3)"), row=8, col=3)
+        sizer.Add(wxTextCtrl(self, -1, "(10,1) colspan: 4"), flag=wxEXPAND, pos=(10,1), colspan=4)
+        sizer.Add(wxTextCtrl(self, -1, "(3,5) rowspan: 8, growable col", style=wxTE_MULTILINE),
+                  flag=wxEXPAND, pos=(3,5), size=(8,1))
+
+        box = wxBoxSizer(wxVERTICAL)
+        box.Add(wxButton(self, -1, "A vertical box"), flag=wxEXPAND)
+        box.Add(wxButton(self, -1, "sizer put in the"), flag=wxEXPAND)
+        box.Add(wxButton(self, -1, "RowColSizer at (12,1)"), flag=wxEXPAND)
+        sizer.Add(box, pos=(12,1))
+
+        sizer.Add(wxTextCtrl(self, -1, "(12,2) align bottom"), flag=wxALIGN_BOTTOM, pos=(12,2))
+        sizer.Add(wxTextCtrl(self, -1, "(12,3) align center"), flag=wxALIGN_CENTER_VERTICAL, pos=(12,3))
+        sizer.Add(wxTextCtrl(self, -1, "(12,4)"),pos=(12,4))
+        sizer.Add(wxTextCtrl(self, -1, "(12,5) full border"), flag=wxEXPAND|wxALL, border=15, pos=(12,5))
+
+        sizer.AddGrowableCol(5)
+        sizer.AddGrowableRow(9)
+
+        sizer.AddSpacer(10,10, pos=(1,6))
+        sizer.AddSpacer(10,10, pos=(13,1))
+
+        self.SetSizer(sizer)
+        self.SetAutoLayout(true)
+
+
+#----------------------------------------------------------------------
+
+def runTest(frame, nb, log):
+    win = TestPanel(nb)
+    return win
+
+
+#----------------------------------------------------------------------
+
+
+import wxPython.lib.rcsizer
+overview = wxPython.lib.rcsizer.__doc__
+
+
 
 class wxBitmap : public wxGDIObject
 {
 public:
-    wxBitmap(const wxString& name, wxBitmapType type=wxBITMAP_TYPE_BMP);
+    wxBitmap(const wxString& name, wxBitmapType type=wxBITMAP_TYPE_ANY);
     ~wxBitmap();
 
     wxPalette* GetPalette();
     wxMask* GetMask();
-    bool LoadFile(const wxString& name, wxBitmapType type=wxBITMAP_TYPE_BMP);
+    bool LoadFile(const wxString& name, wxBitmapType type=wxBITMAP_TYPE_ANY);
     bool SaveFile(const wxString& name, wxBitmapType type, wxPalette* palette = NULL);
     void SetMask(wxMask* mask);
 #ifdef __WXMSW__
 
 #----------------------------------------------------------------------
 
 """
-In this module you will find wxGridSizer and wxFlexGridSizer.
+In this module you will find wxGridSizer and wxFlexGridSizer.  Please
+note that these sizers have since been ported to C++ and those
+versions are now exposed in the regular wxPython wrappers.  However I
+am also leaving them here in the library so they can serve as an
+example of how to implement sizers in Python.
 
 wxGridSizer: Sizes and positions items such that all rows are the same
 height and all columns are the same width.  You can specify a gap in
 
--- /dev/null
+#----------------------------------------------------------------------
+# Name:        wxPython.lib.rcsizer
+# Purpose:     RowColSizer:
+#
+# Author:      Robin Dunn, adapted from code by Niki Spahiev
+#
+# Created:     26-Feb-2002
+# RCS-ID:      $Id$
+# Copyright:   (c) 2002 by Total Control Software
+# Licence:     wxWindows license
+#----------------------------------------------------------------------
+
+"""
+A pure-Python wxSizer that lays out items in a grid similar to
+wxFlexGridSizer but item position is not implicit but explicitly
+specified by row and col, and row/col spanning is supported.
+
+Adapted from code by Niki Spahiev.
+"""
+
+
+
+from wxPython.wx import *
+import operator
+
+#----------------------------------------------------------------------
+
+class RowColSizer(wxPySizer):
+
+    # default sizes for cells with no item
+    col_w = 10
+    row_h = 22
+
+    def __init__(self):
+        wxPySizer.__init__(self)
+        self.growableRows = []
+        self.growableCols = []
+
+
+    def AddGrowableRow(self, idx):
+        self.growableRows.append(idx)
+
+    def AddGrowableCol(self, idx):
+        self.growableCols.append(idx)
+
+
+
+    #--------------------------------------------------
+    def Add(self, item, option=0, flag=0, border=0,
+            row=-1, col=-1,       # row, col and spanning can be specified individually...
+            rowspan=1, colspan=1,
+            pos=None, size=None,  # or as tuples (row,col) and (rowspan,colspan)
+            ):
+
+        if pos is not None:
+            row, col = pos
+        if size is not None:
+            rowspan, colspan = size
+
+        assert row != -1, "Row must be specified"
+        assert col != -1, "Column must be specified"
+
+        # Do I really want to do this?  Probably not...
+        #if rowspan > 1 or colspan > 1:
+        #    flag = flag | wxEXPAND
+
+        wxPySizer.Add(self, item, option, flag, border,
+                      userData=(row, col, row+rowspan, col+colspan))
+
+    #AddWindow = Add
+    #AddSizer  = Add
+
+    def AddSpacer(self, width, height, option=0, flag=0, border=0,
+                  row=-1, col=-1,
+                  rowspan=1, colspan=1,
+                  pos=None, size=None,
+                  ):
+        if pos is not None:
+            row, col = pos
+        if size is not None:
+            rowspan, colspan = size
+
+        assert row != -1, "Row must be specified"
+        assert col != -1, "Column must be specified"
+
+        wxPySizer.AddSpacer(self, width, height, option, flag, border,
+                            userData=(row, col, row+rowspan, col+colspan))
+
+    #--------------------------------------------------
+    def _add( self, size, dim ):
+        r, c, r2, c2 = dim   # unpack coords and spanning
+
+        # are the widths and heights lists long enough?
+        if r2 > len(self.rowHeights):
+            x = [self.row_h] * (r2-len(self.rowHeights))
+            self.rowHeights.extend( x )
+        if c2 > len(self.colWidths):
+            x = [self.col_w] * (c2-len(self.colWidths))
+            self.colWidths.extend( x )
+
+        # set the widths and heights lists for this item
+        scale = (r2 - r)
+        for i in range(r, r2):
+            self.rowHeights[i] = max( self.rowHeights[i], size.height / scale )
+        scale = (c2 - c)
+        for i in range(c, c2):
+            self.colWidths[i] = max( self.colWidths[i], size.width / scale )
+
+
+    #--------------------------------------------------
+    def CalcMin( self ):
+        items = self.GetChildren()
+        if not items:
+            return wxSize(10, 10)
+
+        self.rowHeights = []
+        self.colWidths = []
+
+        for item in items:
+            self._add( item.CalcMin(), item.GetUserData() )
+
+        size = wxSize( reduce( operator.add, self.colWidths),
+                       reduce( operator.add, self.rowHeights) )
+        return size
+
+
+    #--------------------------------------------------
+    def RecalcSizes( self ):
+        # save current dimensions, etc.
+        curWidth  = self.GetSize().width
+        curHeight = self.GetSize().height
+        px = self.GetPosition().x
+        py = self.GetPosition().y
+        minWidth  = self.CalcMin().width
+        minHeight = self.CalcMin().height
+
+        # Check for growables
+        if self.growableRows and curHeight > minHeight:
+            delta = (curHeight - minHeight) / len(self.growableRows)
+            extra = (curHeight - minHeight) % len(self.growableRows)
+            for idx in self.growableRows:
+                self.rowHeights[idx] += delta
+            self.rowHeights[self.growableRows[0]] += extra
+
+        if self.growableCols and curWidth > minWidth:
+            delta = (curWidth - minWidth) / len(self.growableCols)
+            extra = (curWidth - minWidth) % len(self.growableCols)
+            for idx in self.growableCols:
+                self.colWidths[idx] += delta
+            self.colWidths[self.growableCols[0]] += extra
+
+        rpos = [0] * len(self.rowHeights)
+        cpos = [0] * len(self.colWidths)
+
+        for i in range(len(self.rowHeights)):
+            height = self.rowHeights[i]
+            rpos[i] = px
+            px += height
+
+        for i in range(len(self.colWidths)):
+            width = self.colWidths[i]
+            cpos[i] = py
+            py += width
+
+        # iterate children and set dimensions...
+        for item in self.GetChildren():
+            r, c, r2, c2 = item.GetUserData()
+            width = reduce( operator.add, self.colWidths[c:c2] )
+            height = reduce( operator.add, self.rowHeights[r:r2] )
+            #item.SetDimension( (cpos[c], rpos[r]), (width, height))
+            self.SetItemBounds( item, cpos[c], rpos[r], width, height )
+
+
+    #--------------------------------------------------
+    def SetItemBounds(self, item, x, y, w, h):
+        # calculate the item's actual size and position within
+        # its grid cell
+        ipt = wxPoint(x, y)
+        isz = item.CalcMin()
+        flag = item.GetFlag()
+
+        if flag & wxEXPAND or flag & wxSHAPED:
+            isz = wxSize(w, h)
+        else:
+            if flag & wxALIGN_CENTER_HORIZONTAL:
+                ipt.x = x + (w - isz.width) / 2
+            elif flag & wxALIGN_RIGHT:
+                ipt.x = x + (w - isz.width)
+
+            if flag & wxALIGN_CENTER_VERTICAL:
+                ipt.y = y + (h - isz.height) / 2
+            elif flag & wxALIGN_BOTTOM:
+                ipt.y = y + (h - isz.height)
+
+        item.SetDimension(ipt, isz)
+
+
+#----------------------------------------------------------------------
+#----------------------------------------------------------------------
+
+
+
+
+##     #--------------------------------------------------
+##     def _add( self, size, opt, dim ):
+##         r,c,r2,c2 = dim
+##         if r2 > len(self.rows0):
+##             x = [self.row_h] * (r2-len(self.rows0))
+##             self.rows0.extend( x )
+##             self.rows1.extend( x )
+##         if c2 > len(self.cols0):
+##             x = [self.col_w] * (c2-len(self.cols0))
+##             self.cols0.extend( x )
+##             self.cols1.extend( x )
+##         if opt == 0:    # fixed
+##             scale = (r2-r)
+##             for i in range(r,r2):
+##                 self.rows1[i] = self.rows0[i] = max( self.rows0[i], size.y/scale )
+##             scale = (c2-c)
+##             for i in range(c,c2):
+##                 self.cols1[i] = self.cols0[i] = max( self.cols0[i], size.x/scale )
+##         else:
+##             scale = (r2-r)
+##             for i in range(r,r2):
+##                 self.rows0[i] = max( self.rows0[i], size.y/scale )
+##                 self.rows1[i] = self.rows0[i] * opt
+##             scale = (c2-c)
+##             for i in range(c,c2):
+##                 self.cols0[i] = max( self.cols0[i], size.x/scale )
+##                 self.cols1[i] = self.cols0[i] * opt
+
+
+##     #--------------------------------------------------
+##     def CalcMin( self ):
+##         children = self.GetChildren()
+##         if not children:
+##                 return wxSize(10, 10)
+
+##         self.rows0 = []
+##         self.cols0 = []
+##         self.rows1 = []
+##         self.cols1 = []
+
+##         for cell in children:
+##             self._add( cell.CalcMin(), cell.GetOption(), cell.GetUserData() )
+
+##         self.minWidth    = reduce( operator.add, self.cols1 )
+##         self.minHeight   = reduce( operator.add, self.rows1 )
+##         self.fixedWidth  = reduce( operator.add, self.cols0 )   # size without stretched widgets
+##         self.fixedHeight = reduce( operator.add, self.rows0 )
+
+##         return wxSize( self.minWidth, self.minHeight )
+
+
+##     #--------------------------------------------------
+##     def RecalcSizes( self ):
+##         # get current dimensions, save for performance
+##         myWidth  = self.GetSize().x
+##         myHeight = self.GetSize().y
+
+##         # relative recent positions
+##         px = self.GetPosition().x
+##         py = self.GetPosition().y
+
+##         # calculate space for one stretched item
+##         stretchC = 0
+##         for i in range(len(self.cols0)):
+##             if self.cols1[i] <> self.cols0[i]:
+##                 stretchC = stretchC + self.cols1[i] / self.cols0[i]
+##         if myWidth > self.fixedWidth and stretchC:
+##             deltaw = (myWidth - self.fixedWidth) / stretchC
+##             extraw = (myWidth - self.fixedWidth) % stretchC
+##         else:
+##             deltaw = extraw = 0
+
+##         stretchR = 0
+##         for i in range(len(self.rows0)):
+##             if self.rows1[i] <> self.rows0[i]:
+##                 stretchR = stretchR + self.rows1[i] / self.rows0[i]
+##         if myHeight > self.fixedHeight and stretchR:
+##             deltah = (myHeight - self.fixedHeight) / stretchR
+##             extrah = (myHeight - self.fixedHeight) % stretchR
+##         else:
+##             deltah = extrah = 0
+
+##         self.rpos = [0] * len( self.rows0 )
+##         self.cpos = [0] * len( self.cols0 )
+
+##         for i in range(len(self.rows0)):
+##             newHeight = self.rows0[i]
+##             if self.rows1[i] <> self.rows0[i]:
+##                 weight = self.rows1[i] / self.rows0[i]
+##                 # first stretchable gets extra pixels
+##                 newHeight = newHeight + (deltah * weight) + extrah
+##                 extrah = 0
+##             self.rpos[i] = py
+##             self.rows1[i] = newHeight
+##             py = py + newHeight
+
+##         for i in range(len(self.cols0)):
+##             newWidth = self.cols0[i]
+##             if self.cols1[i] <> self.cols0[i]:
+##                 weight = self.cols1[i] / self.cols0[i]
+##                 # first stretchable gets extra pixels
+##                 newWidth = newWidth + (deltaw * weight) + extraw
+##                 extraw = 0
+##             self.cpos[i] = px
+##             self.cols1[i] = newWidth
+##             px = px + newWidth
+
+##         # iterate children ...
+##         for cell in self.GetChildren():
+##             r,c,r2,c2 = cell.GetUserData()
+##             newWidth = reduce( operator.add, self.cols1[c:c2] )
+##             newHeight = reduce( operator.add, self.rows1[r:r2] )
+##             cell.SetDimension( (self.cpos[c], self.rpos[r]), (newWidth, newHeight) )
+
 
--- /dev/null
+# sheet.py
+# CSheet - A wxPython spreadsheet class.
+# This is free software.  Feel free to adapt it as you like.
+# Author: Mark F. Russo (russomf@hotmail.com) 2002/01/31
+
+from wxPython.wx import *
+from wxPython.grid import *
+from string import *
+
+#---------------------------------------------------------------------------
+class CTextCellEditor(wxTextCtrl):
+    """ Custom text control for cell editing """
+    def __init__(self, parent, id, grid):
+        wxTextCtrl.__init__(self, parent, id, "", style=wxNO_BORDER)
+        self._grid = grid                           # Save grid reference
+        EVT_CHAR(self, self.OnChar)
+
+    def OnChar(self, evt):                          # Hook OnChar for custom behavior
+        """Customizes char events """
+        key = evt.GetKeyCode()
+        if   key == WXK_DOWN:
+            self._grid.DisableCellEditControl()     # Commit the edit
+            self._grid.MoveCursorDown(false)        # Change the current cell
+        elif key == WXK_UP:
+            self._grid.DisableCellEditControl()     # Commit the edit
+            self._grid.MoveCursorUp(false)          # Change the current cell
+        elif key == WXK_LEFT:
+            self._grid.DisableCellEditControl()     # Commit the edit
+            self._grid.MoveCursorLeft(false)        # Change the current cell
+        elif key == WXK_RIGHT:
+            self._grid.DisableCellEditControl()     # Commit the edit
+            self._grid.MoveCursorRight(false)       # Change the current cell
+            
+        evt.Skip()                                  # Continue event
+        
+#---------------------------------------------------------------------------
+class CCellEditor(wxPyGridCellEditor):
+    """ Custom cell editor """
+    def __init__(self, grid):
+        wxPyGridCellEditor.__init__(self)
+        self._grid = grid                           # Save a reference to the grid
+        
+    def Create(self, parent, id, evtHandler):
+        """ Create the actual edit control.  Must derive from wxControl.
+            Must Override
+        """
+        self._tc = CTextCellEditor(parent, id, self._grid)
+        self._tc.SetInsertionPoint(0)
+        self.SetControl(self._tc)
+        if evtHandler:
+            self._tc.PushEventHandler(evtHandler)
+
+    def SetSize(self, rect):
+        """ Position/size the edit control within the cell rectangle. """
+        # Size text control to exactly overlay in-cell editing
+        self._tc.SetDimensions(rect.x+3, rect.y+3, rect.width-2, rect.height-2)
+
+    def Show(self, show, attr):
+        """ Show or hide the edit control.  Use the attr (if not None)
+            to set colors or fonts for the control.
+        """
+        self.base_Show(show, attr)
+
+    def PaintBackground(self, rect, attr):
+        """ Draws the part of the cell not occupied by the edit control.  The
+            base class version just fills it with background colour from the
+            attribute.
+        """
+        # Call base class method.
+        self.base_PaintBackground(self, rect, attr)
+    
+    def BeginEdit(self, row, col, grid):
+        """ Fetch the value from the table and prepare edit control to begin editing.
+            Set the focus to the edit control.  Must Override.
+        """
+        self._startValue = grid.GetTable().GetValue(row, col)
+        self._tc.SetValue(self._startValue)
+        self._tc.SetFocus()
+        
+        # Select the text when initiating an edit so that subsequent typing
+        # replaces the contents.
+        self._tc.SetSelection(0, self._tc.GetLastPosition())
+        
+    def EndEdit(self, row, col, grid):
+        """ Commit editing the current cell. Returns true if the value has changed.
+            If necessary, the control may be destroyed. Must Override.
+        """
+        changed = false                             # Assume value not changed
+        val = self._tc.GetValue()                   # Get value in edit control
+        if val != self._startValue:                 # Compare
+            changed = true                          # If different then changed is true
+            grid.GetTable().SetValue(row, col, val) # Update the table
+        self._startValue = ''                       # Clear the class' start value
+        self._tc.SetValue('')                       # Clear contents of the edit control
+
+        return changed
+
+    def Reset(self):
+        """ Reset the value in the control back to its starting value. Must Override. """
+        self._tc.SetValue(self._startValue)
+        self._tc.SetInsertionPointEnd()
+
+    def IsAcceptedKey(self, evt):
+        """ Return true to allow the given key to start editing.  The base class
+            version only checks that the event has no modifiers.  F2 is special
+            and will always start the editor.
+        """
+        return (not (evt.ControlDown() or evt.AltDown())
+                and  evt.GetKeyCode() != WXK_SHIFT)
+
+    def StartingKey(self, evt):
+        """ If the editor is enabled by pressing keys on the grid, this will be
+            called to let the editor react to that first key.
+        """
+        key = evt.GetKeyCode()              # Get the key code
+        ch = None                           # Handle num pad keys
+        if key in [WXK_NUMPAD0, WXK_NUMPAD1, WXK_NUMPAD2, WXK_NUMPAD3, WXK_NUMPAD4,
+                   WXK_NUMPAD5, WXK_NUMPAD6, WXK_NUMPAD7, WXK_NUMPAD8, WXK_NUMPAD9]:
+            ch = chr(ord('0') + key - WXK_NUMPAD0)
+        
+        elif key == WXK_BACK:               # Empty text control when init w/ back key
+            ch = ""
+                                            # Handle normal keys
+        elif key < 256 and key >= 0 and chr(key) in string.printable:
+            ch = chr(key)
+            if not evt.ShiftDown():
+                ch = string.lower(ch)
+        
+        if ch is not None:                  # If are at this point with a key,
+            self._tc.SetValue(ch)           # replace the contents of the text control.
+            self._tc.SetInsertionPointEnd() # Move to the end so that subsequent keys are appended
+        else:
+            evt.Skip()
+
+    def StartingClick(self):
+        """ If the editor is enabled by clicking on the cell, this method will be
+            called to allow the editor to simulate the click on the control.
+        """
+        pass
+
+    def Destroy(self):
+        """ Final cleanup """
+        self.base_Destroy()
+
+    def Clone(self):
+        """ Create a new object which is the copy of this one. Must Override. """
+        return CCellEditor()
+
+#---------------------------------------------------------------------------
+class CSheet(wxGrid):
+    def __init__(self, parent):
+        wxGrid.__init__(self, parent, -1)
+        
+        # Init variables
+        self._lastCol = -1              # Init last cell column clicked
+        self._lastRow = -1              # Init last cell row clicked
+        self._selected = None           # Init range currently selected
+                                        # Map string datatype to default renderer/editor
+        self.RegisterDataType(wxGRID_VALUE_STRING,
+                              wxGridCellStringRenderer(),
+                              CCellEditor(self))
+        
+        self.CreateGrid(4, 3)           # By default start with a 4 x 3 grid
+        self.SetColLabelSize(18)        # Default sizes and alignment
+        self.SetRowLabelSize(50)
+        self.SetRowLabelAlignment(wxALIGN_RIGHT, wxALIGN_BOTTOM)
+        self.SetColSize(0, 75)          # Default column sizes
+        self.SetColSize(1, 75)
+        self.SetColSize(2, 75)
+        
+        # Sink events        
+        EVT_GRID_CELL_LEFT_CLICK(  self, self.OnLeftClick)
+        EVT_GRID_CELL_RIGHT_CLICK( self, self.OnRightClick)
+        EVT_GRID_CELL_LEFT_DCLICK( self, self.OnLeftDoubleClick)
+        EVT_GRID_RANGE_SELECT(     self, self.OnRangeSelect)
+        EVT_GRID_ROW_SIZE(         self, self.OnRowSize)
+        EVT_GRID_COL_SIZE(         self, self.OnColSize)
+        EVT_GRID_CELL_CHANGE(      self, self.OnCellChange)
+        EVT_GRID_SELECT_CELL(      self, self.OnGridSelectCell)
+
+    def OnGridSelectCell(self, event):
+        """ Track cell selections """
+        # Save the last cell coordinates
+        self._lastRow, self._lastCol = event.GetRow(), event.GetCol()
+        event.Skip()
+        
+    def OnRowSize(self, event):
+        event.Skip()
+
+    def OnColSize(self, event):
+        event.Skip()
+
+    def OnCellChange(self, event):
+        event.Skip()
+    
+    def OnLeftClick(self, event):
+        """ Override left-click behavior to prevent left-click edit initiation """
+        # Save the cell clicked
+        currCell = (event.GetRow(), event.GetCol())
+        
+        # Suppress event if same cell clicked twice in a row.
+        # This prevents a single-click from initiating an edit.
+        if currCell != (self._lastRow, self._lastCol): event.Skip()
+        
+    def OnRightClick(self, event):
+        """ Move grid cursor when a cell is right-clicked """
+        self.SetGridCursor( event.GetRow(), event.GetCol() )
+        event.Skip()
+        
+    def OnLeftDoubleClick(self, event):
+        """ Initiate the cell editor on a double-click """
+        # Move grid cursor to double-clicked cell
+        if self.CanEnableCellControl():
+            self.SetGridCursor( event.GetRow(), event.GetCol() )
+            self.EnableCellEditControl(true)    # Show the cell editor
+        event.Skip()
+
+    def OnRangeSelect(self, event):
+        """ Track which cells are selected so that copy/paste behavior can be implemented """
+        # If a single cell is selected, then Selecting() returns false (0)
+        # and range coords are entire grid.  In this case cancel previous selection.
+        # If more than one cell is selected, then Selecting() is true (1)
+        # and range accurately reflects selected cells.  Save them.
+        # If more cells are added to a selection, selecting remains true (1)
+        self._selected = None
+        if event.Selecting():
+            self._selected = ((event.GetTopRow(), event.GetLeftCol()),
+                              (event.GetBottomRow(), event.GetRightCol()))
+        event.Skip()
+        
+    def Copy(self):
+        """ Copy the currently selected cells to the clipboard """
+        # TODO: raise an error when there are no cells selected?
+        if self._selected == None: return
+        ((r1, c1), (r2, c2)) = self._selected
+        
+        # Build a string to put on the clipboard
+        # (Is there a faster way to do this in Python?)
+        crlf = chr(13) + chr(10)
+        tab = chr(9)
+        s = ""
+        for row in range(r1, r2+1):
+            for col in range(c1, c2):
+                s += self.GetCellValue(row,col)
+                s += tab
+            s += self.GetCellValue(row, c2)
+            s += crlf
+        
+        # Put the string on the clipboard
+        if wxTheClipboard.Open():
+            wxTheClipboard.Clear()
+            wxTheClipboard.SetData(wxTextDataObject(s))
+            wxTheClipboard.Close()
+
+    def Paste(self):
+        """ Paste the contents of the clipboard into the currently selected cells """
+        # (Is there a better way to do this?)
+        if wxTheClipboard.Open():
+            td = wxTextDataObject()
+            success = wxTheClipboard.GetData(td)
+            wxTheClipboard.Close()
+            if not success: return              # Exit on failure
+            s = td.GetText()                    # Get the text
+            
+            crlf = chr(13) + chr(10)            # CrLf characters
+            tab = chr(9)                        # Tab character
+            
+            rows = split(s, crlf)               # split into rows
+            rows = rows[0:-1]                   # leave out last element, which is always empty
+            for i in range(0, len(rows)):       # split rows into elements
+                rows[i] = split(rows[i], tab)
+            
+            # Get the starting and ending cell range to paste into
+            if self._selected == None:          # If no cells selected...
+                r1 = self.GetGridCursorRow()    # Start the paste at the current location
+                c1 = self.GetGridCursorCol()
+                r2 = self.GetNumberRows()-1     # Go to maximum row and col extents
+                c2 = self.GetNumberCols()-1
+            else:                               # If cells selected, only paste there
+                ((r1, c1), (r2, c2)) = self._selected
+            
+            # Enter data into spreadsheet cells one at a time
+            r = r1                              # Init row and column counters
+            c = c1
+            for row in rows:                    # Loop over all rows
+                for element in row:             # Loop over all row elements
+                    self.SetCellValue(r, c, str(element))   # Set cell value
+                    c += 1                      # Increment the column counter
+                    if c > c2: break            # Do not exceed maximum column
+                r += 1
+                if r > r2: break                # Do not exceed maximum row
+                c = c1
+
+    def Clear(self):
+        """ Clear the currently selected cells """
+        if self._selected == None:              # If no selection...
+            r = self.GetGridCursorRow()         # clear only current cell
+            c = self.GetGridCursorCol()
+            self.SetCellValue(r, c, "")
+        else:                                   # Otherwise clear selected cells
+            ((r1, c1), (r2, c2)) = self._selected
+            for r in range(r1, r2+1):
+                for c in range(c1, c2+1):
+                    self.SetCellValue(r, c, "")
+
+    def SetNumberRows(self, numRows=1):
+        """ Set the number of rows in the sheet """
+        # Check for non-negative number
+        if numRows < 0:  return false
+        
+        # Adjust number of rows
+        curRows = self.GetNumberRows()
+        if curRows < numRows:
+            self.AppendRows(numRows - curRows)
+        elif curRows > numRows:
+            self.DeleteRows(numRows, curRows - numRows)
+
+        return true
+
+    def SetNumberCols(self, numCols=1):
+        """ Set the number of columns in the sheet """
+        # Check for non-negative number
+        if numCols < 0:  return false
+        
+        # Adjust number of rows
+        curCols = self.GetNumberCols()
+        if curCols < numCols:
+            self.AppendCols(numCols - curCols)
+        elif curCols > numCols:
+            self.DeleteCols(numCols, curCols - numCols)
+
+        return true