+#---------------------------------------------------------------------------
+class CTextCellEditor(wx.TextCtrl):
+ """ Custom text control for cell editing """
+ def __init__(self, parent, id, grid):
+ wx.TextCtrl.__init__(self, parent, id, "", style=wx.NO_BORDER)
+ self._grid = grid # Save grid reference
+ self.Bind(wx.EVT_CHAR, self.OnChar)
+
+ def OnChar(self, evt): # Hook OnChar for custom behavior
+ """Customizes char events """
+ key = evt.GetKeyCode()
+ if key == wx.WXK_DOWN:
+ self._grid.DisableCellEditControl() # Commit the edit
+ self._grid.MoveCursorDown(False) # Change the current cell
+ elif key == wx.WXK_UP:
+ self._grid.DisableCellEditControl() # Commit the edit
+ self._grid.MoveCursorUp(False) # Change the current cell
+ elif key == wx.WXK_LEFT:
+ self._grid.DisableCellEditControl() # Commit the edit
+ self._grid.MoveCursorLeft(False) # Change the current cell
+ elif key == wx.WXK_RIGHT:
+ self._grid.DisableCellEditControl() # Commit the edit
+ self._grid.MoveCursorRight(False) # Change the current cell
+
+ evt.Skip() # Continue event
+
+#---------------------------------------------------------------------------
+class CCellEditor(wx.grid.PyGridCellEditor):
+ """ Custom cell editor """
+ def __init__(self, grid):
+ wx.grid.PyGridCellEditor.__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() != wx.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 [ wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3,
+ wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7,
+ wx.WXK_NUMPAD8, wx.WXK_NUMPAD9]:
+ ch = chr(ord('0') + key - wx.WXK_NUMPAD0)
+
+ elif key == wx.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 = ch.lower()
+
+ 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(wx.grid.Grid):
+ def __init__(self, parent):
+ wx.grid.Grid.__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(wx.grid.GRID_VALUE_STRING,
+ wx.grid.GridCellStringRenderer(),
+ 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(wx.ALIGN_RIGHT, wx.ALIGN_BOTTOM)
+ self.SetColSize(0, 75) # Default column sizes
+ self.SetColSize(1, 75)
+ self.SetColSize(2, 75)
+
+ # Sink events
+ self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnLeftClick)
+ self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.OnRightClick)
+ self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.OnLeftDoubleClick)
+ self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self.OnRangeSelect)
+ self.Bind(wx.grid.EVT_GRID_ROW_SIZE, self.OnRowSize)
+ self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.OnColSize)
+ self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange)
+ self.Bind(wx.grid.EVT_GRID_SELECT_CELL, 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 wx.TheClipboard.Open():
+ wx.TheClipboard.Clear()
+ wx.TheClipboard.SetData(wx.TextDataObject(s))
+ wx.TheClipboard.Close()
+
+ def Paste(self):
+ """ Paste the contents of the clipboard into the currently selected cells """
+ # (Is there a better way to do this?)
+ if wx.TheClipboard.Open():
+ td = wx.TextDataObject()
+ success = wx.TheClipboard.GetData(td)
+ wx.TheClipboard.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 = s.split(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] = rows[i].split(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