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