X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/8b9a4190f70909de9568f45389e7aa3ecbc66b8a..eb3efe0d4b8a0964cd0504a69c0e9540c18e390c:/wxPython/wx/lib/sheet.py diff --git a/wxPython/wx/lib/sheet.py b/wxPython/wx/lib/sheet.py index 8e05440f44..e177a3a35e 100644 --- a/wxPython/wx/lib/sheet.py +++ b/wxPython/wx/lib/sheet.py @@ -1,8 +1,346 @@ +# 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 +#--------------------------------------------------------------------------- +# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o 2.5 compatability update. +# o Untested. +# -"""Renamer stub: provides a way to drop the wx prefix from wxPython objects.""" +import string +import wx +import wx.grid -from wx import _rename -from wxPython.lib import sheet -_rename(globals(), sheet.__dict__, modulename='lib.sheet') -del sheet -del _rename +#--------------------------------------------------------------------------- +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. + NOTE: There is no need to everride this if you don't need + to do something out of the ordinary. + """ + super(CCellEditor, self).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. + NOTE: There is no need to everride this if you don't need + to do something out of the ordinary. + """ + # Call base class method. + super(CCellEditor, self).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 + NOTE: There is no need to everride this if you don't need + to do something out of the ordinary. + """ + super(CCellEditor, self).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