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