]>
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. | |
ac1c82d7 | 67 | NOTE: There is no need to everride this if you don't need |
a7a01418 | 68 | to do something out of the ordinary. |
d14a1e28 | 69 | """ |
a7a01418 | 70 | super(CCellEditor, self).Show(show, attr) |
d14a1e28 RD |
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. | |
ac1c82d7 | 76 | NOTE: There is no need to everride this if you don't need |
a7a01418 | 77 | to do something out of the ordinary. |
d14a1e28 RD |
78 | """ |
79 | # Call base class method. | |
a7a01418 | 80 | super(CCellEditor, self).PaintBackground(self, rect, attr) |
d14a1e28 RD |
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()) | |
b881fc78 | 119 | and evt.GetKeyCode() != wx.WXK_SHIFT) |
d14a1e28 RD |
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 | |
b881fc78 RD |
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) | |
d14a1e28 | 131 | |
b881fc78 | 132 | elif key == wx.WXK_BACK: # Empty text control when init w/ back key |
d14a1e28 RD |
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): | |
ac1c82d7 RD |
153 | """ Final cleanup |
154 | NOTE: There is no need to everride this if you don't need | |
a7a01418 | 155 | to do something out of the ordinary. |
ac1c82d7 | 156 | """ |
a7a01418 | 157 | super(CCellEditor, self).Destroy() |
d14a1e28 RD |
158 | |
159 | def Clone(self): | |
160 | """ Create a new object which is the copy of this one. Must Override. """ | |
161 | return CCellEditor() | |
162 | ||
163 | #--------------------------------------------------------------------------- | |
b881fc78 | 164 | class CSheet(wx.grid.Grid): |
d14a1e28 | 165 | def __init__(self, parent): |
b881fc78 | 166 | wx.grid.Grid.__init__(self, parent, -1) |
d14a1e28 RD |
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 | |
b881fc78 RD |
173 | self.RegisterDataType(wx.grid.GRID_VALUE_STRING, |
174 | wx.grid.GridCellStringRenderer(), | |
d14a1e28 RD |
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) | |
b881fc78 | 180 | self.SetRowLabelAlignment(wx.ALIGN_RIGHT, wx.ALIGN_BOTTOM) |
d14a1e28 RD |
181 | self.SetColSize(0, 75) # Default column sizes |
182 | self.SetColSize(1, 75) | |
183 | self.SetColSize(2, 75) | |
184 | ||
185 | # Sink events | |
b881fc78 RD |
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) | |
d14a1e28 RD |
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 | |
b881fc78 RD |
264 | if wx.TheClipboard.Open(): |
265 | wx.TheClipboard.Clear() | |
266 | wx.TheClipboard.SetData(wx.TextDataObject(s)) | |
267 | wx.TheClipboard.Close() | |
d14a1e28 RD |
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?) | |
b881fc78 RD |
272 | if wx.TheClipboard.Open(): |
273 | td = wx.TextDataObject() | |
274 | success = wx.TheClipboard.GetData(td) | |
275 | wx.TheClipboard.Close() | |
d14a1e28 RD |
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 |