| 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 | NOTE: There is no need to everride this if you don't need |
| 68 | to do something out of the ordingary. |
| 69 | """ |
| 70 | self.base_Show(show, attr) |
| 71 | |
| 72 | def PaintBackground(self, rect, attr): |
| 73 | """ Draws the part of the cell not occupied by the edit control. The |
| 74 | base class version just fills it with background colour from the |
| 75 | attribute. |
| 76 | NOTE: There is no need to everride this if you don't need |
| 77 | to do something out of the ordingary. |
| 78 | """ |
| 79 | # Call base class method. |
| 80 | self.base_PaintBackground(self, rect, attr) |
| 81 | |
| 82 | def BeginEdit(self, row, col, grid): |
| 83 | """ Fetch the value from the table and prepare edit control to begin editing. |
| 84 | Set the focus to the edit control. Must Override. |
| 85 | """ |
| 86 | self._startValue = grid.GetTable().GetValue(row, col) |
| 87 | self._tc.SetValue(self._startValue) |
| 88 | self._tc.SetFocus() |
| 89 | |
| 90 | # Select the text when initiating an edit so that subsequent typing |
| 91 | # replaces the contents. |
| 92 | self._tc.SetSelection(0, self._tc.GetLastPosition()) |
| 93 | |
| 94 | def EndEdit(self, row, col, grid): |
| 95 | """ Commit editing the current cell. Returns True if the value has changed. |
| 96 | If necessary, the control may be destroyed. Must Override. |
| 97 | """ |
| 98 | changed = False # Assume value not changed |
| 99 | val = self._tc.GetValue() # Get value in edit control |
| 100 | if val != self._startValue: # Compare |
| 101 | changed = True # If different then changed is True |
| 102 | grid.GetTable().SetValue(row, col, val) # Update the table |
| 103 | self._startValue = '' # Clear the class' start value |
| 104 | self._tc.SetValue('') # Clear contents of the edit control |
| 105 | |
| 106 | return changed |
| 107 | |
| 108 | def Reset(self): |
| 109 | """ Reset the value in the control back to its starting value. Must Override. """ |
| 110 | self._tc.SetValue(self._startValue) |
| 111 | self._tc.SetInsertionPointEnd() |
| 112 | |
| 113 | def IsAcceptedKey(self, evt): |
| 114 | """ Return True to allow the given key to start editing. The base class |
| 115 | version only checks that the event has no modifiers. F2 is special |
| 116 | and will always start the editor. |
| 117 | """ |
| 118 | return (not (evt.ControlDown() or evt.AltDown()) |
| 119 | and evt.GetKeyCode() != wx.WXK_SHIFT) |
| 120 | |
| 121 | def StartingKey(self, evt): |
| 122 | """ If the editor is enabled by pressing keys on the grid, this will be |
| 123 | called to let the editor react to that first key. |
| 124 | """ |
| 125 | key = evt.GetKeyCode() # Get the key code |
| 126 | ch = None # Handle num pad keys |
| 127 | if key in [ wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, |
| 128 | wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, |
| 129 | wx.WXK_NUMPAD8, wx.WXK_NUMPAD9]: |
| 130 | ch = chr(ord('0') + key - wx.WXK_NUMPAD0) |
| 131 | |
| 132 | elif key == wx.WXK_BACK: # Empty text control when init w/ back key |
| 133 | ch = "" |
| 134 | # Handle normal keys |
| 135 | elif key < 256 and key >= 0 and chr(key) in string.printable: |
| 136 | ch = chr(key) |
| 137 | if not evt.ShiftDown(): |
| 138 | ch = ch.lower() |
| 139 | |
| 140 | if ch is not None: # If are at this point with a key, |
| 141 | self._tc.SetValue(ch) # replace the contents of the text control. |
| 142 | self._tc.SetInsertionPointEnd() # Move to the end so that subsequent keys are appended |
| 143 | else: |
| 144 | evt.Skip() |
| 145 | |
| 146 | def StartingClick(self): |
| 147 | """ If the editor is enabled by clicking on the cell, this method will be |
| 148 | called to allow the editor to simulate the click on the control. |
| 149 | """ |
| 150 | pass |
| 151 | |
| 152 | def Destroy(self): |
| 153 | """ Final cleanup |
| 154 | NOTE: There is no need to everride this if you don't need |
| 155 | to do something out of the ordingary. |
| 156 | """ |
| 157 | self.base_Destroy() |
| 158 | |
| 159 | def Clone(self): |
| 160 | """ Create a new object which is the copy of this one. Must Override. """ |
| 161 | return CCellEditor() |
| 162 | |
| 163 | #--------------------------------------------------------------------------- |
| 164 | class CSheet(wx.grid.Grid): |
| 165 | def __init__(self, parent): |
| 166 | wx.grid.Grid.__init__(self, parent, -1) |
| 167 | |
| 168 | # Init variables |
| 169 | self._lastCol = -1 # Init last cell column clicked |
| 170 | self._lastRow = -1 # Init last cell row clicked |
| 171 | self._selected = None # Init range currently selected |
| 172 | # Map string datatype to default renderer/editor |
| 173 | self.RegisterDataType(wx.grid.GRID_VALUE_STRING, |
| 174 | wx.grid.GridCellStringRenderer(), |
| 175 | CCellEditor(self)) |
| 176 | |
| 177 | self.CreateGrid(4, 3) # By default start with a 4 x 3 grid |
| 178 | self.SetColLabelSize(18) # Default sizes and alignment |
| 179 | self.SetRowLabelSize(50) |
| 180 | self.SetRowLabelAlignment(wx.ALIGN_RIGHT, wx.ALIGN_BOTTOM) |
| 181 | self.SetColSize(0, 75) # Default column sizes |
| 182 | self.SetColSize(1, 75) |
| 183 | self.SetColSize(2, 75) |
| 184 | |
| 185 | # Sink events |
| 186 | self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnLeftClick) |
| 187 | self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.OnRightClick) |
| 188 | self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.OnLeftDoubleClick) |
| 189 | self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self.OnRangeSelect) |
| 190 | self.Bind(wx.grid.EVT_GRID_ROW_SIZE, self.OnRowSize) |
| 191 | self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.OnColSize) |
| 192 | self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange) |
| 193 | self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnGridSelectCell) |
| 194 | |
| 195 | def OnGridSelectCell(self, event): |
| 196 | """ Track cell selections """ |
| 197 | # Save the last cell coordinates |
| 198 | self._lastRow, self._lastCol = event.GetRow(), event.GetCol() |
| 199 | event.Skip() |
| 200 | |
| 201 | def OnRowSize(self, event): |
| 202 | event.Skip() |
| 203 | |
| 204 | def OnColSize(self, event): |
| 205 | event.Skip() |
| 206 | |
| 207 | def OnCellChange(self, event): |
| 208 | event.Skip() |
| 209 | |
| 210 | def OnLeftClick(self, event): |
| 211 | """ Override left-click behavior to prevent left-click edit initiation """ |
| 212 | # Save the cell clicked |
| 213 | currCell = (event.GetRow(), event.GetCol()) |
| 214 | |
| 215 | # Suppress event if same cell clicked twice in a row. |
| 216 | # This prevents a single-click from initiating an edit. |
| 217 | if currCell != (self._lastRow, self._lastCol): event.Skip() |
| 218 | |
| 219 | def OnRightClick(self, event): |
| 220 | """ Move grid cursor when a cell is right-clicked """ |
| 221 | self.SetGridCursor( event.GetRow(), event.GetCol() ) |
| 222 | event.Skip() |
| 223 | |
| 224 | def OnLeftDoubleClick(self, event): |
| 225 | """ Initiate the cell editor on a double-click """ |
| 226 | # Move grid cursor to double-clicked cell |
| 227 | if self.CanEnableCellControl(): |
| 228 | self.SetGridCursor( event.GetRow(), event.GetCol() ) |
| 229 | self.EnableCellEditControl(True) # Show the cell editor |
| 230 | event.Skip() |
| 231 | |
| 232 | def OnRangeSelect(self, event): |
| 233 | """ Track which cells are selected so that copy/paste behavior can be implemented """ |
| 234 | # If a single cell is selected, then Selecting() returns False (0) |
| 235 | # and range coords are entire grid. In this case cancel previous selection. |
| 236 | # If more than one cell is selected, then Selecting() is True (1) |
| 237 | # and range accurately reflects selected cells. Save them. |
| 238 | # If more cells are added to a selection, selecting remains True (1) |
| 239 | self._selected = None |
| 240 | if event.Selecting(): |
| 241 | self._selected = ((event.GetTopRow(), event.GetLeftCol()), |
| 242 | (event.GetBottomRow(), event.GetRightCol())) |
| 243 | event.Skip() |
| 244 | |
| 245 | def Copy(self): |
| 246 | """ Copy the currently selected cells to the clipboard """ |
| 247 | # TODO: raise an error when there are no cells selected? |
| 248 | if self._selected == None: return |
| 249 | ((r1, c1), (r2, c2)) = self._selected |
| 250 | |
| 251 | # Build a string to put on the clipboard |
| 252 | # (Is there a faster way to do this in Python?) |
| 253 | crlf = chr(13) + chr(10) |
| 254 | tab = chr(9) |
| 255 | s = "" |
| 256 | for row in range(r1, r2+1): |
| 257 | for col in range(c1, c2): |
| 258 | s += self.GetCellValue(row,col) |
| 259 | s += tab |
| 260 | s += self.GetCellValue(row, c2) |
| 261 | s += crlf |
| 262 | |
| 263 | # Put the string on the clipboard |
| 264 | if wx.TheClipboard.Open(): |
| 265 | wx.TheClipboard.Clear() |
| 266 | wx.TheClipboard.SetData(wx.TextDataObject(s)) |
| 267 | wx.TheClipboard.Close() |
| 268 | |
| 269 | def Paste(self): |
| 270 | """ Paste the contents of the clipboard into the currently selected cells """ |
| 271 | # (Is there a better way to do this?) |
| 272 | if wx.TheClipboard.Open(): |
| 273 | td = wx.TextDataObject() |
| 274 | success = wx.TheClipboard.GetData(td) |
| 275 | wx.TheClipboard.Close() |
| 276 | if not success: return # Exit on failure |
| 277 | s = td.GetText() # Get the text |
| 278 | |
| 279 | crlf = chr(13) + chr(10) # CrLf characters |
| 280 | tab = chr(9) # Tab character |
| 281 | |
| 282 | rows = s.split(crlf) # split into rows |
| 283 | rows = rows[0:-1] # leave out last element, which is always empty |
| 284 | for i in range(0, len(rows)): # split rows into elements |
| 285 | rows[i] = rows[i].split(tab) |
| 286 | |
| 287 | # Get the starting and ending cell range to paste into |
| 288 | if self._selected == None: # If no cells selected... |
| 289 | r1 = self.GetGridCursorRow() # Start the paste at the current location |
| 290 | c1 = self.GetGridCursorCol() |
| 291 | r2 = self.GetNumberRows()-1 # Go to maximum row and col extents |
| 292 | c2 = self.GetNumberCols()-1 |
| 293 | else: # If cells selected, only paste there |
| 294 | ((r1, c1), (r2, c2)) = self._selected |
| 295 | |
| 296 | # Enter data into spreadsheet cells one at a time |
| 297 | r = r1 # Init row and column counters |
| 298 | c = c1 |
| 299 | for row in rows: # Loop over all rows |
| 300 | for element in row: # Loop over all row elements |
| 301 | self.SetCellValue(r, c, str(element)) # Set cell value |
| 302 | c += 1 # Increment the column counter |
| 303 | if c > c2: break # Do not exceed maximum column |
| 304 | r += 1 |
| 305 | if r > r2: break # Do not exceed maximum row |
| 306 | c = c1 |
| 307 | |
| 308 | def Clear(self): |
| 309 | """ Clear the currently selected cells """ |
| 310 | if self._selected == None: # If no selection... |
| 311 | r = self.GetGridCursorRow() # clear only current cell |
| 312 | c = self.GetGridCursorCol() |
| 313 | self.SetCellValue(r, c, "") |
| 314 | else: # Otherwise clear selected cells |
| 315 | ((r1, c1), (r2, c2)) = self._selected |
| 316 | for r in range(r1, r2+1): |
| 317 | for c in range(c1, c2+1): |
| 318 | self.SetCellValue(r, c, "") |
| 319 | |
| 320 | def SetNumberRows(self, numRows=1): |
| 321 | """ Set the number of rows in the sheet """ |
| 322 | # Check for non-negative number |
| 323 | if numRows < 0: return False |
| 324 | |
| 325 | # Adjust number of rows |
| 326 | curRows = self.GetNumberRows() |
| 327 | if curRows < numRows: |
| 328 | self.AppendRows(numRows - curRows) |
| 329 | elif curRows > numRows: |
| 330 | self.DeleteRows(numRows, curRows - numRows) |
| 331 | |
| 332 | return True |
| 333 | |
| 334 | def SetNumberCols(self, numCols=1): |
| 335 | """ Set the number of columns in the sheet """ |
| 336 | # Check for non-negative number |
| 337 | if numCols < 0: return False |
| 338 | |
| 339 | # Adjust number of rows |
| 340 | curCols = self.GetNumberCols() |
| 341 | if curCols < numCols: |
| 342 | self.AppendCols(numCols - curCols) |
| 343 | elif curCols > numCols: |
| 344 | self.DeleteCols(numCols, curCols - numCols) |
| 345 | |
| 346 | return True |