| 1 | # sheet.py |
| 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) |
| 7 | # |
| 8 | # o 2.5 compatability update. |
| 9 | # o Untested. |
| 10 | # |
| 11 | |
| 12 | import string |
| 13 | import wx |
| 14 | import wx.grid |
| 15 | |
| 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) |
| 23 | |
| 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 |
| 39 | |
| 40 | evt.Skip() # Continue event |
| 41 | |
| 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 |
| 48 | |
| 49 | def Create(self, parent, id, evtHandler): |
| 50 | """ Create the actual edit control. Must derive from wxControl. |
| 51 | Must Override |
| 52 | """ |
| 53 | self._tc = CTextCellEditor(parent, id, self._grid) |
| 54 | self._tc.SetInsertionPoint(0) |
| 55 | self.SetControl(self._tc) |
| 56 | if evtHandler: |
| 57 | self._tc.PushEventHandler(evtHandler) |
| 58 | |
| 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) |
| 63 | |
| 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. |
| 67 | |
| 68 | NOTE: There is no need to everride this if you don't need |
| 69 | to do something out of the ordinary. |
| 70 | """ |
| 71 | super(CCellEditor, self).Show(show, attr) |
| 72 | |
| 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 |
| 76 | attribute. |
| 77 | |
| 78 | NOTE: There is no need to everride this if you don't need |
| 79 | to do something out of the ordinary. |
| 80 | """ |
| 81 | # Call base class method. |
| 82 | super(CCellEditor, self).PaintBackground(self, rect, attr) |
| 83 | |
| 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. |
| 87 | """ |
| 88 | self._startValue = grid.GetTable().GetValue(row, col) |
| 89 | self._tc.SetValue(self._startValue) |
| 90 | self._tc.SetFocus() |
| 91 | |
| 92 | # Select the text when initiating an edit so that subsequent typing |
| 93 | # replaces the contents. |
| 94 | self._tc.SetSelection(0, self._tc.GetLastPosition()) |
| 95 | |
| 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. |
| 99 | """ |
| 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 |
| 107 | |
| 108 | return changed |
| 109 | |
| 110 | def Reset(self): |
| 111 | """ Reset the value in the control back to its starting value. Must Override. """ |
| 112 | self._tc.SetValue(self._startValue) |
| 113 | self._tc.SetInsertionPointEnd() |
| 114 | |
| 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. |
| 119 | """ |
| 120 | return (not (evt.ControlDown() or evt.AltDown()) |
| 121 | and evt.GetKeyCode() != wx.WXK_SHIFT) |
| 122 | |
| 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. |
| 126 | """ |
| 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) |
| 133 | |
| 134 | elif key == wx.WXK_BACK: # Empty text control when init w/ back key |
| 135 | ch = "" |
| 136 | # Handle normal keys |
| 137 | elif key < 256 and key >= 0 and chr(key) in string.printable: |
| 138 | ch = chr(key) |
| 139 | if not evt.ShiftDown(): |
| 140 | ch = ch.lower() |
| 141 | |
| 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 |
| 145 | else: |
| 146 | evt.Skip() |
| 147 | |
| 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. |
| 151 | """ |
| 152 | pass |
| 153 | |
| 154 | def Destroy(self): |
| 155 | """ Final cleanup |
| 156 | |
| 157 | NOTE: There is no need to everride this if you don't need |
| 158 | to do something out of the ordinary. |
| 159 | """ |
| 160 | super(CCellEditor, self).Destroy() |
| 161 | |
| 162 | def Clone(self): |
| 163 | """ Create a new object which is the copy of this one. Must Override. """ |
| 164 | return CCellEditor() |
| 165 | |
| 166 | #--------------------------------------------------------------------------- |
| 167 | class CSheet(wx.grid.Grid): |
| 168 | def __init__(self, parent): |
| 169 | wx.grid.Grid.__init__(self, parent, -1) |
| 170 | |
| 171 | # Init variables |
| 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(), |
| 178 | CCellEditor(self)) |
| 179 | |
| 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) |
| 187 | |
| 188 | # Sink events |
| 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) |
| 197 | |
| 198 | def OnGridSelectCell(self, event): |
| 199 | """ Track cell selections """ |
| 200 | # Save the last cell coordinates |
| 201 | self._lastRow, self._lastCol = event.GetRow(), event.GetCol() |
| 202 | event.Skip() |
| 203 | |
| 204 | def OnRowSize(self, event): |
| 205 | event.Skip() |
| 206 | |
| 207 | def OnColSize(self, event): |
| 208 | event.Skip() |
| 209 | |
| 210 | def OnCellChange(self, event): |
| 211 | event.Skip() |
| 212 | |
| 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()) |
| 217 | |
| 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() |
| 221 | |
| 222 | def OnRightClick(self, event): |
| 223 | """ Move grid cursor when a cell is right-clicked """ |
| 224 | self.SetGridCursor( event.GetRow(), event.GetCol() ) |
| 225 | event.Skip() |
| 226 | |
| 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 |
| 233 | event.Skip() |
| 234 | |
| 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())) |
| 246 | event.Skip() |
| 247 | |
| 248 | def Copy(self): |
| 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 |
| 253 | |
| 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) |
| 257 | tab = chr(9) |
| 258 | s = "" |
| 259 | for row in range(r1, r2+1): |
| 260 | for col in range(c1, c2): |
| 261 | s += self.GetCellValue(row,col) |
| 262 | s += tab |
| 263 | s += self.GetCellValue(row, c2) |
| 264 | s += crlf |
| 265 | |
| 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() |
| 271 | |
| 272 | def Paste(self): |
| 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 |
| 281 | |
| 282 | crlf = chr(13) + chr(10) # CrLf characters |
| 283 | tab = chr(9) # Tab character |
| 284 | |
| 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) |
| 289 | |
| 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 |
| 298 | |
| 299 | # Enter data into spreadsheet cells one at a time |
| 300 | r = r1 # Init row and column counters |
| 301 | c = c1 |
| 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 |
| 307 | r += 1 |
| 308 | if r > r2: break # Do not exceed maximum row |
| 309 | c = c1 |
| 310 | |
| 311 | def Clear(self): |
| 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, "") |
| 322 | |
| 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 |
| 327 | |
| 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) |
| 334 | |
| 335 | return True |
| 336 | |
| 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 |
| 341 | |
| 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) |
| 348 | |
| 349 | return True |