File: sheet.py

package info (click to toggle)
wxpython3.0 3.0.2.0%2Bdfsg-4
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 482,760 kB
  • ctags: 518,293
  • sloc: cpp: 2,127,226; python: 294,045; makefile: 51,942; ansic: 19,033; sh: 3,013; xml: 1,629; perl: 17
file content (349 lines) | stat: -rw-r--r-- 15,097 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# sheet.py
# CSheet - A wxPython spreadsheet class.
# This is free software.  Feel free to adapt it as you like.
# Author: Mark F. Russo (russomf@hotmail.com) 2002/01/31
#---------------------------------------------------------------------------
# 12/11/2003 - Jeff Grimmett (grimmtooth@softhome.net)
#
# o 2.5 compatability update.
# o Untested.
#

import  string
import  wx
import  wx.grid

#---------------------------------------------------------------------------
class CTextCellEditor(wx.TextCtrl):
    """ Custom text control for cell editing """
    def __init__(self, parent, id, grid):
        wx.TextCtrl.__init__(self, parent, id, "", style=wx.NO_BORDER)
        self._grid = grid                           # Save grid reference
        self.Bind(wx.EVT_CHAR, self.OnChar)

    def OnChar(self, evt):                          # Hook OnChar for custom behavior
        """Customizes char events """
        key = evt.GetKeyCode()
        if   key == wx.WXK_DOWN:
            self._grid.DisableCellEditControl()     # Commit the edit
            self._grid.MoveCursorDown(False)        # Change the current cell
        elif key == wx.WXK_UP:
            self._grid.DisableCellEditControl()     # Commit the edit
            self._grid.MoveCursorUp(False)          # Change the current cell
        elif key == wx.WXK_LEFT:
            self._grid.DisableCellEditControl()     # Commit the edit
            self._grid.MoveCursorLeft(False)        # Change the current cell
        elif key == wx.WXK_RIGHT:
            self._grid.DisableCellEditControl()     # Commit the edit
            self._grid.MoveCursorRight(False)       # Change the current cell

        evt.Skip()                                  # Continue event

#---------------------------------------------------------------------------
class CCellEditor(wx.grid.PyGridCellEditor):
    """ Custom cell editor """
    def __init__(self, grid):
        wx.grid.PyGridCellEditor.__init__(self)
        self._grid = grid                           # Save a reference to the grid

    def Create(self, parent, id, evtHandler):
        """ Create the actual edit control.  Must derive from wxControl.
            Must Override
        """
        self._tc = CTextCellEditor(parent, id, self._grid)
        self._tc.SetInsertionPoint(0)
        self.SetControl(self._tc)
        if evtHandler:
            self._tc.PushEventHandler(evtHandler)

    def SetSize(self, rect):
        """ Position/size the edit control within the cell rectangle. """
        # Size text control to exactly overlay in-cell editing
        self._tc.SetDimensions(rect.x+3, rect.y+3, rect.width-2, rect.height-2)

    def Show(self, show, attr):
        """ Show or hide the edit control.  Use the attr (if not None)
            to set colors or fonts for the control.

            NOTE: There is no need to everride this if you don't need
            to do something out of the ordinary.
        """
        super(CCellEditor, self).Show(show, attr)

    def PaintBackground(self, rect, attr):
        """ Draws the part of the cell not occupied by the edit control.  The
            base class version just fills it with background colour from the
            attribute.

            NOTE: There is no need to everride this if you don't need
            to do something out of the ordinary.
        """
        # Call base class method.
        super(CCellEditor, self).PaintBackground(rect, attr)

    def BeginEdit(self, row, col, grid):
        """ Fetch the value from the table and prepare edit control to begin editing.
            Set the focus to the edit control.  Must Override.
        """
        self._startValue = grid.GetTable().GetValue(row, col)
        self._tc.SetValue(self._startValue)
        self._tc.SetFocus()

        # Select the text when initiating an edit so that subsequent typing
        # replaces the contents.
        self._tc.SetSelection(0, self._tc.GetLastPosition())

    def EndEdit(self, row, col, grid):
        """ Commit editing the current cell. Returns True if the value has changed.
            If necessary, the control may be destroyed. Must Override.
        """
        changed = False                             # Assume value not changed
        val = self._tc.GetValue()                   # Get value in edit control
        if val != self._startValue:                 # Compare
            changed = True                          # If different then changed is True
            grid.GetTable().SetValue(row, col, val) # Update the table
        self._startValue = ''                       # Clear the class' start value
        self._tc.SetValue('')                       # Clear contents of the edit control

        return changed

    def Reset(self):
        """ Reset the value in the control back to its starting value. Must Override. """
        self._tc.SetValue(self._startValue)
        self._tc.SetInsertionPointEnd()

    def IsAcceptedKey(self, evt):
        """ Return True to allow the given key to start editing.  The base class
            version only checks that the event has no modifiers.  F2 is special
            and will always start the editor.
        """
        return (not (evt.ControlDown() or evt.AltDown())
                and  evt.GetKeyCode() != wx.WXK_SHIFT)

    def StartingKey(self, evt):
        """ If the editor is enabled by pressing keys on the grid, this will be
            called to let the editor react to that first key.
        """
        key = evt.GetKeyCode()              # Get the key code
        ch = None                           # Handle num pad keys
        if key in [ wx.WXK_NUMPAD0, wx.WXK_NUMPAD1, wx.WXK_NUMPAD2, wx.WXK_NUMPAD3, 
                    wx.WXK_NUMPAD4, wx.WXK_NUMPAD5, wx.WXK_NUMPAD6, wx.WXK_NUMPAD7, 
                    wx.WXK_NUMPAD8, wx.WXK_NUMPAD9]:
            ch = chr(ord('0') + key - wx.WXK_NUMPAD0)

        elif key == wx.WXK_BACK:               # Empty text control when init w/ back key
            ch = ""
                                            # Handle normal keys
        elif key < 256 and key >= 0 and chr(key) in string.printable:
            ch = chr(key)
            if not evt.ShiftDown():
                ch = ch.lower()

        if ch is not None:                  # If are at this point with a key,
            self._tc.SetValue(ch)           # replace the contents of the text control.
            self._tc.SetInsertionPointEnd() # Move to the end so that subsequent keys are appended
        else:
            evt.Skip()

    def StartingClick(self):
        """ If the editor is enabled by clicking on the cell, this method will be
            called to allow the editor to simulate the click on the control.
        """
        pass

    def Destroy(self):
        """ Final cleanup
        
            NOTE: There is no need to everride this if you don't need
            to do something out of the ordinary.
        """
        super(CCellEditor, self).Destroy()

    def Clone(self):
        """ Create a new object which is the copy of this one. Must Override. """
        return CCellEditor()

#---------------------------------------------------------------------------
class CSheet(wx.grid.Grid):
    def __init__(self, parent):
        wx.grid.Grid.__init__(self, parent, -1)

        # Init variables
        self._lastCol = -1              # Init last cell column clicked
        self._lastRow = -1              # Init last cell row clicked
        self._selected = None           # Init range currently selected
                                        # Map string datatype to default renderer/editor
        self.RegisterDataType(wx.grid.GRID_VALUE_STRING,
                              wx.grid.GridCellStringRenderer(),
                              CCellEditor(self))

        self.CreateGrid(4, 3)           # By default start with a 4 x 3 grid
        self.SetColLabelSize(18)        # Default sizes and alignment
        self.SetRowLabelSize(50)
        self.SetRowLabelAlignment(wx.ALIGN_RIGHT, wx.ALIGN_BOTTOM)
        self.SetColSize(0, 75)          # Default column sizes
        self.SetColSize(1, 75)
        self.SetColSize(2, 75)

        # Sink events
        self.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnLeftClick)
        self.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.OnRightClick)
        self.Bind(wx.grid.EVT_GRID_CELL_LEFT_DCLICK, self.OnLeftDoubleClick)
        self.Bind(wx.grid.EVT_GRID_RANGE_SELECT, self.OnRangeSelect)
        self.Bind(wx.grid.EVT_GRID_ROW_SIZE, self.OnRowSize)
        self.Bind(wx.grid.EVT_GRID_COL_SIZE, self.OnColSize)
        self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange)
        self.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnGridSelectCell)

    def OnGridSelectCell(self, event):
        """ Track cell selections """
        # Save the last cell coordinates
        self._lastRow, self._lastCol = event.GetRow(), event.GetCol()
        event.Skip()

    def OnRowSize(self, event):
        event.Skip()

    def OnColSize(self, event):
        event.Skip()

    def OnCellChange(self, event):
        event.Skip()

    def OnLeftClick(self, event):
        """ Override left-click behavior to prevent left-click edit initiation """
        # Save the cell clicked
        currCell = (event.GetRow(), event.GetCol())

        # Suppress event if same cell clicked twice in a row.
        # This prevents a single-click from initiating an edit.
        if currCell != (self._lastRow, self._lastCol): event.Skip()

    def OnRightClick(self, event):
        """ Move grid cursor when a cell is right-clicked """
        self.SetGridCursor( event.GetRow(), event.GetCol() )
        event.Skip()

    def OnLeftDoubleClick(self, event):
        """ Initiate the cell editor on a double-click """
        # Move grid cursor to double-clicked cell
        if self.CanEnableCellControl():
            self.SetGridCursor( event.GetRow(), event.GetCol() )
            self.EnableCellEditControl(True)    # Show the cell editor
        event.Skip()

    def OnRangeSelect(self, event):
        """ Track which cells are selected so that copy/paste behavior can be implemented """
        # If a single cell is selected, then Selecting() returns False (0)
        # and range coords are entire grid.  In this case cancel previous selection.
        # If more than one cell is selected, then Selecting() is True (1)
        # and range accurately reflects selected cells.  Save them.
        # If more cells are added to a selection, selecting remains True (1)
        self._selected = None
        if event.Selecting():
            self._selected = ((event.GetTopRow(), event.GetLeftCol()),
                              (event.GetBottomRow(), event.GetRightCol()))
        event.Skip()

    def Copy(self):
        """ Copy the currently selected cells to the clipboard """
        # TODO: raise an error when there are no cells selected?
        if self._selected == None: return
        ((r1, c1), (r2, c2)) = self._selected

        # Build a string to put on the clipboard
        # (Is there a faster way to do this in Python?)
        crlf = chr(13) + chr(10)
        tab = chr(9)
        s = ""
        for row in range(r1, r2+1):
            for col in range(c1, c2):
                s += self.GetCellValue(row,col)
                s += tab
            s += self.GetCellValue(row, c2)
            s += crlf

        # Put the string on the clipboard
        if wx.TheClipboard.Open():
            wx.TheClipboard.Clear()
            wx.TheClipboard.SetData(wx.TextDataObject(s))
            wx.TheClipboard.Close()

    def Paste(self):
        """ Paste the contents of the clipboard into the currently selected cells """
        # (Is there a better way to do this?)
        if wx.TheClipboard.Open():
            td = wx.TextDataObject()
            success = wx.TheClipboard.GetData(td)
            wx.TheClipboard.Close()
            if not success: return              # Exit on failure
            s = td.GetText()                    # Get the text

            crlf = chr(13) + chr(10)            # CrLf characters
            tab = chr(9)                        # Tab character

            rows = s.split(crlf)               # split into rows
            rows = rows[0:-1]                   # leave out last element, which is always empty
            for i in range(0, len(rows)):       # split rows into elements
                rows[i] = rows[i].split(tab)

            # Get the starting and ending cell range to paste into
            if self._selected == None:          # If no cells selected...
                r1 = self.GetGridCursorRow()    # Start the paste at the current location
                c1 = self.GetGridCursorCol()
                r2 = self.GetNumberRows()-1     # Go to maximum row and col extents
                c2 = self.GetNumberCols()-1
            else:                               # If cells selected, only paste there
                ((r1, c1), (r2, c2)) = self._selected

            # Enter data into spreadsheet cells one at a time
            r = r1                              # Init row and column counters
            c = c1
            for row in rows:                    # Loop over all rows
                for element in row:             # Loop over all row elements
                    self.SetCellValue(r, c, str(element))   # Set cell value
                    c += 1                      # Increment the column counter
                    if c > c2: break            # Do not exceed maximum column
                r += 1
                if r > r2: break                # Do not exceed maximum row
                c = c1

    def Clear(self):
        """ Clear the currently selected cells """
        if self._selected == None:              # If no selection...
            r = self.GetGridCursorRow()         # clear only current cell
            c = self.GetGridCursorCol()
            self.SetCellValue(r, c, "")
        else:                                   # Otherwise clear selected cells
            ((r1, c1), (r2, c2)) = self._selected
            for r in range(r1, r2+1):
                for c in range(c1, c2+1):
                    self.SetCellValue(r, c, "")

    def SetNumberRows(self, numRows=1):
        """ Set the number of rows in the sheet """
        # Check for non-negative number
        if numRows < 0:  return False

        # Adjust number of rows
        curRows = self.GetNumberRows()
        if curRows < numRows:
            self.AppendRows(numRows - curRows)
        elif curRows > numRows:
            self.DeleteRows(numRows, curRows - numRows)

        return True

    def SetNumberCols(self, numCols=1):
        """ Set the number of columns in the sheet """
        # Check for non-negative number
        if numCols < 0:  return False

        # Adjust number of rows
        curCols = self.GetNumberCols()
        if curCols < numCols:
            self.AppendCols(numCols - curCols)
        elif curCols > numCols:
            self.DeleteCols(numCols, curCols - numCols)

        return True