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