]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/sheet.py
Oops, DoLog and DoLogString are protected
[wxWidgets.git] / wxPython / wx / lib / sheet.py
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 ordinary.
69         """
70         super(CCellEditor, self).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 ordinary.
78         """
79         # Call base class method.
80         super(CCellEditor, self).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 ordinary.
156         """
157         super(CCellEditor, self).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