]>
Commit | Line | Data |
---|---|---|
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 |