2 # CSheet - A wxPython spreadsheet class. 
   3 # This is free software.  Feel free to adapt it as you like. 
   4 # Author: Mark F. Russo (russomf@hotmail.com) 2002/01/31 
   5 #--------------------------------------------------------------------------- 
   6 # 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
   8 # o 2.5 compatability update. 
  16 #--------------------------------------------------------------------------- 
  17 class CTextCellEditor(wx
.TextCtrl
): 
  18     """ Custom text control for cell editing """ 
  19     def __init__(self
, parent
, id, grid
): 
  20         wx
.TextCtrl
.__init
__(self
, parent
, id, "", style
=wx
.NO_BORDER
) 
  21         self
._grid 
= grid                           
# Save grid reference 
  22         self
.Bind(wx
.EVT_CHAR
, self
.OnChar
) 
  24     def OnChar(self
, evt
):                          # Hook OnChar for custom behavior 
  25         """Customizes char events """ 
  26         key 
= evt
.GetKeyCode() 
  27         if   key 
== wx
.WXK_DOWN
: 
  28             self
._grid
.DisableCellEditControl()     # Commit the edit 
  29             self
._grid
.MoveCursorDown(False)        # Change the current cell 
  30         elif key 
== wx
.WXK_UP
: 
  31             self
._grid
.DisableCellEditControl()     # Commit the edit 
  32             self
._grid
.MoveCursorUp(False)          # Change the current cell 
  33         elif key 
== wx
.WXK_LEFT
: 
  34             self
._grid
.DisableCellEditControl()     # Commit the edit 
  35             self
._grid
.MoveCursorLeft(False)        # Change the current cell 
  36         elif key 
== wx
.WXK_RIGHT
: 
  37             self
._grid
.DisableCellEditControl()     # Commit the edit 
  38             self
._grid
.MoveCursorRight(False)       # Change the current cell 
  40         evt
.Skip()                                  # Continue event 
  42 #--------------------------------------------------------------------------- 
  43 class CCellEditor(wx
.grid
.PyGridCellEditor
): 
  44     """ Custom cell editor """ 
  45     def __init__(self
, grid
): 
  46         wx
.grid
.PyGridCellEditor
.__init
__(self
) 
  47         self
._grid 
= grid                           
# Save a reference to the grid 
  49     def Create(self
, parent
, id, evtHandler
): 
  50         """ Create the actual edit control.  Must derive from wxControl. 
  53         self
._tc 
= CTextCellEditor(parent
, id, self
._grid
) 
  54         self
._tc
.SetInsertionPoint(0) 
  55         self
.SetControl(self
._tc
) 
  57             self
._tc
.PushEventHandler(evtHandler
) 
  59     def SetSize(self
, rect
): 
  60         """ Position/size the edit control within the cell rectangle. """ 
  61         # Size text control to exactly overlay in-cell editing 
  62         self
._tc
.SetDimensions(rect
.x
+3, rect
.y
+3, rect
.width
-2, rect
.height
-2) 
  64     def Show(self
, show
, attr
): 
  65         """ Show or hide the edit control.  Use the attr (if not None) 
  66             to set colors or fonts for the control. 
  68             NOTE: There is no need to everride this if you don't need 
  69             to do something out of the ordinary. 
  71         super(CCellEditor
, self
).Show(show
, attr
) 
  73     def PaintBackground(self
, rect
, attr
): 
  74         """ Draws the part of the cell not occupied by the edit control.  The 
  75             base class version just fills it with background colour from the 
  78             NOTE: There is no need to everride this if you don't need 
  79             to do something out of the ordinary. 
  81         # Call base class method. 
  82         super(CCellEditor
, self
).PaintBackground(self
, rect
, attr
) 
  84     def BeginEdit(self
, row
, col
, grid
): 
  85         """ Fetch the value from the table and prepare edit control to begin editing. 
  86             Set the focus to the edit control.  Must Override. 
  88         self
._startValue 
= grid
.GetTable().GetValue(row
, col
) 
  89         self
._tc
.SetValue(self
._startValue
) 
  92         # Select the text when initiating an edit so that subsequent typing 
  93         # replaces the contents. 
  94         self
._tc
.SetSelection(0, self
._tc
.GetLastPosition()) 
  96     def EndEdit(self
, row
, col
, grid
): 
  97         """ Commit editing the current cell. Returns True if the value has changed. 
  98             If necessary, the control may be destroyed. Must Override. 
 100         changed 
= False                             # Assume value not changed 
 101         val 
= self
._tc
.GetValue()                   # Get value in edit control 
 102         if val 
!= self
._startValue
:                 # Compare 
 103             changed 
= True                          # If different then changed is True 
 104             grid
.GetTable().SetValue(row
, col
, val
) # Update the table 
 105         self
._startValue 
= ''                       # Clear the class' start value 
 106         self
._tc
.SetValue('')                       # Clear contents of the edit control 
 111         """ Reset the value in the control back to its starting value. Must Override. """ 
 112         self
._tc
.SetValue(self
._startValue
) 
 113         self
._tc
.SetInsertionPointEnd() 
 115     def IsAcceptedKey(self
, evt
): 
 116         """ Return True to allow the given key to start editing.  The base class 
 117             version only checks that the event has no modifiers.  F2 is special 
 118             and will always start the editor. 
 120         return (not (evt
.ControlDown() or evt
.AltDown()) 
 121                 and  evt
.GetKeyCode() != wx
.WXK_SHIFT
) 
 123     def StartingKey(self
, evt
): 
 124         """ If the editor is enabled by pressing keys on the grid, this will be 
 125             called to let the editor react to that first key. 
 127         key 
= evt
.GetKeyCode()              # Get the key code 
 128         ch 
= None                           # Handle num pad keys 
 129         if key 
in [ wx
.WXK_NUMPAD0
, wx
.WXK_NUMPAD1
, wx
.WXK_NUMPAD2
, wx
.WXK_NUMPAD3
,  
 130                     wx
.WXK_NUMPAD4
, wx
.WXK_NUMPAD5
, wx
.WXK_NUMPAD6
, wx
.WXK_NUMPAD7
,  
 131                     wx
.WXK_NUMPAD8
, wx
.WXK_NUMPAD9
]: 
 132             ch 
= chr(ord('0') + key 
- wx
.WXK_NUMPAD0
) 
 134         elif key 
== wx
.WXK_BACK
:               # Empty text control when init w/ back key 
 137         elif key 
< 256 and key 
>= 0 and chr(key
) in string
.printable
: 
 139             if not evt
.ShiftDown(): 
 142         if ch 
is not None:                  # If are at this point with a key, 
 143             self
._tc
.SetValue(ch
)           # replace the contents of the text control. 
 144             self
._tc
.SetInsertionPointEnd() # Move to the end so that subsequent keys are appended 
 148     def StartingClick(self
): 
 149         """ If the editor is enabled by clicking on the cell, this method will be 
 150             called to allow the editor to simulate the click on the control. 
 157             NOTE: There is no need to everride this if you don't need 
 158             to do something out of the ordinary. 
 160         super(CCellEditor
, self
).Destroy() 
 163         """ Create a new object which is the copy of this one. Must Override. """ 
 166 #--------------------------------------------------------------------------- 
 167 class CSheet(wx
.grid
.Grid
): 
 168     def __init__(self
, parent
): 
 169         wx
.grid
.Grid
.__init
__(self
, parent
, -1) 
 172         self
._lastCol 
= -1              # Init last cell column clicked 
 173         self
._lastRow 
= -1              # Init last cell row clicked 
 174         self
._selected 
= None           # Init range currently selected 
 175                                         # Map string datatype to default renderer/editor 
 176         self
.RegisterDataType(wx
.grid
.GRID_VALUE_STRING
, 
 177                               wx
.grid
.GridCellStringRenderer(), 
 180         self
.CreateGrid(4, 3)           # By default start with a 4 x 3 grid 
 181         self
.SetColLabelSize(18)        # Default sizes and alignment 
 182         self
.SetRowLabelSize(50) 
 183         self
.SetRowLabelAlignment(wx
.ALIGN_RIGHT
, wx
.ALIGN_BOTTOM
) 
 184         self
.SetColSize(0, 75)          # Default column sizes 
 185         self
.SetColSize(1, 75) 
 186         self
.SetColSize(2, 75) 
 189         self
.Bind(wx
.grid
.EVT_GRID_CELL_LEFT_CLICK
, self
.OnLeftClick
) 
 190         self
.Bind(wx
.grid
.EVT_GRID_CELL_RIGHT_CLICK
, self
.OnRightClick
) 
 191         self
.Bind(wx
.grid
.EVT_GRID_CELL_LEFT_DCLICK
, self
.OnLeftDoubleClick
) 
 192         self
.Bind(wx
.grid
.EVT_GRID_RANGE_SELECT
, self
.OnRangeSelect
) 
 193         self
.Bind(wx
.grid
.EVT_GRID_ROW_SIZE
, self
.OnRowSize
) 
 194         self
.Bind(wx
.grid
.EVT_GRID_COL_SIZE
, self
.OnColSize
) 
 195         self
.Bind(wx
.grid
.EVT_GRID_CELL_CHANGE
, self
.OnCellChange
) 
 196         self
.Bind(wx
.grid
.EVT_GRID_SELECT_CELL
, self
.OnGridSelectCell
) 
 198     def OnGridSelectCell(self
, event
): 
 199         """ Track cell selections """ 
 200         # Save the last cell coordinates 
 201         self
._lastRow
, self
._lastCol 
= event
.GetRow(), event
.GetCol() 
 204     def OnRowSize(self
, event
): 
 207     def OnColSize(self
, event
): 
 210     def OnCellChange(self
, event
): 
 213     def OnLeftClick(self
, event
): 
 214         """ Override left-click behavior to prevent left-click edit initiation """ 
 215         # Save the cell clicked 
 216         currCell 
= (event
.GetRow(), event
.GetCol()) 
 218         # Suppress event if same cell clicked twice in a row. 
 219         # This prevents a single-click from initiating an edit. 
 220         if currCell 
!= (self
._lastRow
, self
._lastCol
): event
.Skip() 
 222     def OnRightClick(self
, event
): 
 223         """ Move grid cursor when a cell is right-clicked """ 
 224         self
.SetGridCursor( event
.GetRow(), event
.GetCol() ) 
 227     def OnLeftDoubleClick(self
, event
): 
 228         """ Initiate the cell editor on a double-click """ 
 229         # Move grid cursor to double-clicked cell 
 230         if self
.CanEnableCellControl(): 
 231             self
.SetGridCursor( event
.GetRow(), event
.GetCol() ) 
 232             self
.EnableCellEditControl(True)    # Show the cell editor 
 235     def OnRangeSelect(self
, event
): 
 236         """ Track which cells are selected so that copy/paste behavior can be implemented """ 
 237         # If a single cell is selected, then Selecting() returns False (0) 
 238         # and range coords are entire grid.  In this case cancel previous selection. 
 239         # If more than one cell is selected, then Selecting() is True (1) 
 240         # and range accurately reflects selected cells.  Save them. 
 241         # If more cells are added to a selection, selecting remains True (1) 
 242         self
._selected 
= None 
 243         if event
.Selecting(): 
 244             self
._selected 
= ((event
.GetTopRow(), event
.GetLeftCol()), 
 245                               (event
.GetBottomRow(), event
.GetRightCol())) 
 249         """ Copy the currently selected cells to the clipboard """ 
 250         # TODO: raise an error when there are no cells selected? 
 251         if self
._selected 
== None: return 
 252         ((r1
, c1
), (r2
, c2
)) = self
._selected
 
 254         # Build a string to put on the clipboard 
 255         # (Is there a faster way to do this in Python?) 
 256         crlf 
= chr(13) + chr(10) 
 259         for row 
in range(r1
, r2
+1): 
 260             for col 
in range(c1
, c2
): 
 261                 s 
+= self
.GetCellValue(row
,col
) 
 263             s 
+= self
.GetCellValue(row
, c2
) 
 266         # Put the string on the clipboard 
 267         if wx
.TheClipboard
.Open(): 
 268             wx
.TheClipboard
.Clear() 
 269             wx
.TheClipboard
.SetData(wx
.TextDataObject(s
)) 
 270             wx
.TheClipboard
.Close() 
 273         """ Paste the contents of the clipboard into the currently selected cells """ 
 274         # (Is there a better way to do this?) 
 275         if wx
.TheClipboard
.Open(): 
 276             td 
= wx
.TextDataObject() 
 277             success 
= wx
.TheClipboard
.GetData(td
) 
 278             wx
.TheClipboard
.Close() 
 279             if not success
: return              # Exit on failure 
 280             s 
= td
.GetText()                    # Get the text 
 282             crlf 
= chr(13) + chr(10)            # CrLf characters 
 283             tab 
= chr(9)                        # Tab character 
 285             rows 
= s
.split(crlf
)               # split into rows 
 286             rows 
= rows
[0:-1]                   # leave out last element, which is always empty 
 287             for i 
in range(0, len(rows
)):       # split rows into elements 
 288                 rows
[i
] = rows
[i
].split(tab
) 
 290             # Get the starting and ending cell range to paste into 
 291             if self
._selected 
== None:          # If no cells selected... 
 292                 r1 
= self
.GetGridCursorRow()    # Start the paste at the current location 
 293                 c1 
= self
.GetGridCursorCol() 
 294                 r2 
= self
.GetNumberRows()-1     # Go to maximum row and col extents 
 295                 c2 
= self
.GetNumberCols()-1 
 296             else:                               # If cells selected, only paste there 
 297                 ((r1
, c1
), (r2
, c2
)) = self
._selected
 
 299             # Enter data into spreadsheet cells one at a time 
 300             r 
= r1                              
# Init row and column counters 
 302             for row 
in rows
:                    # Loop over all rows 
 303                 for element 
in row
:             # Loop over all row elements 
 304                     self
.SetCellValue(r
, c
, str(element
))   # Set cell value 
 305                     c 
+= 1                      # Increment the column counter 
 306                     if c 
> c2
: break            # Do not exceed maximum column 
 308                 if r 
> r2
: break                # Do not exceed maximum row 
 312         """ Clear the currently selected cells """ 
 313         if self
._selected 
== None:              # If no selection... 
 314             r 
= self
.GetGridCursorRow()         # clear only current cell 
 315             c 
= self
.GetGridCursorCol() 
 316             self
.SetCellValue(r
, c
, "") 
 317         else:                                   # Otherwise clear selected cells 
 318             ((r1
, c1
), (r2
, c2
)) = self
._selected
 
 319             for r 
in range(r1
, r2
+1): 
 320                 for c 
in range(c1
, c2
+1): 
 321                     self
.SetCellValue(r
, c
, "") 
 323     def SetNumberRows(self
, numRows
=1): 
 324         """ Set the number of rows in the sheet """ 
 325         # Check for non-negative number 
 326         if numRows 
< 0:  return False 
 328         # Adjust number of rows 
 329         curRows 
= self
.GetNumberRows() 
 330         if curRows 
< numRows
: 
 331             self
.AppendRows(numRows 
- curRows
) 
 332         elif curRows 
> numRows
: 
 333             self
.DeleteRows(numRows
, curRows 
- numRows
) 
 337     def SetNumberCols(self
, numCols
=1): 
 338         """ Set the number of columns in the sheet """ 
 339         # Check for non-negative number 
 340         if numCols 
< 0:  return False 
 342         # Adjust number of rows 
 343         curCols 
= self
.GetNumberCols() 
 344         if curCols 
< numCols
: 
 345             self
.AppendCols(numCols 
- curCols
) 
 346         elif curCols 
> numCols
: 
 347             self
.DeleteCols(numCols
, curCols 
- numCols
)