~launchpad-p-s/sofastatistics/main

« back to all changes in this revision

Viewing changes to table_entry.py

  • Committer: Grant Paton-Simpson
  • Date: 2009-05-19 04:21:43 UTC
  • Revision ID: g@ubuntu-20090519042143-p561mbokz3inefvd
Initial import

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import wx
 
2
import wx.grid
 
3
import pprint
 
4
 
 
5
import text_browser
 
6
 
 
7
COL_STR = "col_string"
 
8
COL_INT = "col_integer"
 
9
COL_FLOAT = "col_float"
 
10
COL_TEXT_BROWSE = "col_button"
 
11
 
 
12
 
 
13
class TableEntryDlg(wx.Dialog):
 
14
    def __init__(self, title, grid_size, col_dets, data, new_grid_data):
 
15
        """
 
16
        col_dets - [(col_type, col_label, col_width), ( , ) ...]
 
17
        data - list of tuples (must have at least one item, even if only a 
 
18
            "rename me".
 
19
        new_grid_data - is effectively "returned".  Add details to it in form 
 
20
            of a list of tuples.
 
21
        """
 
22
        wx.Dialog.__init__(self, None, title=title,
 
23
                          size=(400,400), 
 
24
                          style=wx.RESIZE_BORDER|wx.CAPTION|wx.CLOSE_BOX|
 
25
                              wx.SYSTEM_MENU)
 
26
        self.panel = wx.Panel(self)
 
27
        self.szrMain = wx.BoxSizer(wx.VERTICAL)
 
28
        self.tabentry = TableEntry(self, self.panel, self.szrMain, grid_size, 
 
29
                                   col_dets, data, new_grid_data)
 
30
        # Close
 
31
        self.SetupButtons()
 
32
        # sizers
 
33
        self.szrMain.Add(self.szrButtons, 0, wx.ALL, 10)
 
34
        self.panel.SetSizer(self.szrMain)
 
35
        self.szrMain.SetSizeHints(self)
 
36
        self.Layout()
 
37
        self.tabentry.grid.SetFocus()
 
38
        
 
39
    def SetupButtons(self):
 
40
        "Separated for text_browser reuse"
 
41
        btnCancel = wx.Button(self.panel, wx.ID_CANCEL)
 
42
        btnCancel.Bind(wx.EVT_BUTTON, self.OnCancel)            
 
43
        btnOK = wx.Button(self.panel, wx.ID_OK) # must have ID of wx.ID_OK 
 
44
        # to trigger validators (no event binding needed) and 
 
45
        # for std dialog button layout
 
46
        btnOK.Bind(wx.EVT_BUTTON, self.OnOK)
 
47
        btnOK.SetDefault()
 
48
        # using the approach which will follow the platform convention 
 
49
        # for standard buttons
 
50
        self.szrButtons = wx.StdDialogButtonSizer()
 
51
        self.szrButtons.AddButton(btnCancel)
 
52
        self.szrButtons.AddButton(btnOK)
 
53
        self.szrButtons.Realize()
 
54
 
 
55
    def OnCancel(self, event):
 
56
        self.Destroy()
 
57
        self.SetReturnCode(wx.ID_CANCEL)
 
58
 
 
59
    def OnOK(self, event):
 
60
        self.tabentry.UpdateNewGridData()
 
61
        self.Destroy()
 
62
        self.SetReturnCode(wx.ID_OK)
 
63
    
 
64
                
 
65
class TableEntry(object):
 
66
    def __init__(self, frame, panel, szr, read_only, grid_size, col_dets, 
 
67
                 data, new_grid_data):
 
68
        """
 
69
        data - list of tuples (must have at least one item, even if only a 
 
70
            "rename me".
 
71
        col_dets - [(col_type, col_label), ( , ) ...]
 
72
        new_grid_data - is effectively "returned" - add details to it in form 
 
73
            of a list of tuples.
 
74
        """
 
75
        self.frame = frame
 
76
        self.panel = panel
 
77
        self.szr = szr
 
78
        self.read_only = read_only
 
79
        self.SetupGrid(grid_size, col_dets, data, new_grid_data)
 
80
        self.szr.Add(self.grid, 1, wx.GROW|wx.ALL, 5)
 
81
 
 
82
    def SetupGrid(self, size, col_dets, data, new_grid_data):
 
83
        """
 
84
        Set up grid.  Convenient to separate so can reuse when subclassing,
 
85
            perhaps to add extra controls.
 
86
        col dets - the col det tuple should be label, type, min, max
 
87
        """
 
88
        self.col_dets = col_dets
 
89
        # store any fixed min col_widths
 
90
        self.col_widths = [None for x in range(len(col_dets))] # initialise
 
91
        for col_idx, col_det in enumerate(self.col_dets):
 
92
            if len(col_det) >= 3:
 
93
                self.col_widths[col_idx] = col_det[2]
 
94
        data.sort(key=lambda s: s[0])
 
95
        self.data = data
 
96
        self.new_grid_data = new_grid_data
 
97
        self.prev_vals = []
 
98
        self.new_editor_shown = False
 
99
        # grid control
 
100
        self.grid = wx.grid.Grid(self.panel, size=size)
 
101
        self.rows_n = len(self.data) # data rows only - not inc new entry row
 
102
        self.cols_n = len(self.col_dets)
 
103
        if self.rows_n:
 
104
            data_cols_n = len(data[0])
 
105
            #pprint.pprint(data) # debug
 
106
            if data_cols_n != self.cols_n:
 
107
                raise Exception, "There must be one set of column details " + \
 
108
                    "per column of data (currently %s details for %s columns)" % \
 
109
                    (self.cols_n, data_cols_n)
 
110
        self.grid.CreateGrid(numRows=self.rows_n + 1, # plus data entry row
 
111
                             numCols=self.cols_n)
 
112
        self.grid.EnableEditing(not self.read_only)
 
113
        # Set any col min widths specifically specified
 
114
        for col_idx in range(len(self.col_dets)):
 
115
            col_width = self.col_widths[col_idx]
 
116
            if col_width:
 
117
                self.grid.SetColMinimalWidth(col_idx, col_width)
 
118
                self.grid.SetColSize(col_idx, col_width) # otherwise will only see effect after resizing
 
119
            else:
 
120
                self.grid.AutoSizeColumn(col_idx, setAsMin=False)
 
121
        self.grid.ForceRefresh()
 
122
        # set col rendering and editing (string is default)
 
123
        for col_idx, col_det in enumerate(self.col_dets):
 
124
            col_type = col_det[1]
 
125
            if col_type == COL_INT:
 
126
                self.grid.SetColFormatNumber(col_idx)
 
127
            elif col_type == COL_FLOAT:
 
128
                width, precision = self.GetWidthPrecision(col_idx)
 
129
                self.grid.SetColFormatFloat(col_idx, width, precision)
 
130
            # must set editor cell by cell amazingly
 
131
            for j in range(self.rows_n + 1):
 
132
                renderer, editor = self.GetNewRendererEditor(col_idx)
 
133
                self.grid.SetCellRenderer(j, col_idx, renderer)
 
134
                self.grid.SetCellEditor(j, col_idx, editor)
 
135
        # grid event handling
 
136
        self.grid.Bind(text_browser.EVT_TEXT_BROWSE_KEY_DOWN, 
 
137
                       self.OnTextBrowseKeyDown)        
 
138
        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnCellChange)
 
139
        self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnSelectCell)
 
140
        self.frame.Bind(wx.grid.EVT_GRID_EDITOR_SHOWN, self.EditorShown)
 
141
        self.frame.Bind(wx.grid.EVT_GRID_EDITOR_HIDDEN, self.EditorHidden)
 
142
        self.grid.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
 
143
        self.grid.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.OnLabelClick)
 
144
        for col_idx, col_det in enumerate(self.col_dets):
 
145
            self.grid.SetColLabelValue(col_idx, col_det[0])
 
146
        for i in range(self.rows_n):
 
147
            for j in range(self.cols_n):
 
148
                self.grid.SetCellValue(row=i, col=j, s=str(data[i][j]))
 
149
        self.grid.SetRowLabelValue(self.rows_n, "*")
 
150
        self.grid.SetGridCursor(self.rows_n, 0)
 
151
        self.grid.MakeCellVisible(self.rows_n, 0)
 
152
    
 
153
    def OnTextBrowseKeyDown(self, event):
 
154
        if event.GetKeyCode() in [wx.WXK_RETURN]:
 
155
            self.grid.DisableCellEditControl()
 
156
            self.grid.MoveCursorDown(expandSelection=False)
 
157
    
 
158
    def GetNewRendererEditor(self, col_idx):
 
159
        """
 
160
        For a given column index, return a fresh renderer and editor object.
 
161
        Objects must be unique to cell.
 
162
        Returns renderer, editor.
 
163
        Nearly (but not quite) making classes ;-)
 
164
        """
 
165
        col_type = self.col_dets[col_idx][1]
 
166
        if col_type == COL_INT:
 
167
            try:
 
168
                min = self.col_dets[col_idx][2]
 
169
            except Exception:
 
170
                min = -1
 
171
            try:
 
172
                max = self.col_dets[col_idx][3]
 
173
            except Exception:
 
174
                max = min
 
175
            renderer = wx.grid.GridCellNumberRenderer()
 
176
            editor = wx.grid.GridCellNumberEditor(min, max)
 
177
        elif col_type == COL_FLOAT:
 
178
            width, precision = self.GetWidthPrecision(col_idx)
 
179
            renderer = wx.grid.GridCellFloatRenderer(width, precision)
 
180
            editor = wx.grid.GridCellFloatEditor(width, precision)
 
181
        elif col_type == COL_TEXT_BROWSE:
 
182
            renderer = wx.grid.GridCellStringRenderer()
 
183
            file_phrase = self.col_dets[col_idx][3]
 
184
            if len(self.col_dets[col_idx]) == 5:
 
185
                wildcard = self.col_dets[col_idx][4]
 
186
            else:
 
187
                wildcard = "Any file (*.*)|*.*"
 
188
            editor = text_browser.GridCellTextBrowseEditor(file_phrase, 
 
189
                                                           wildcard)
 
190
        else:
 
191
            renderer = wx.grid.GridCellStringRenderer()
 
192
            editor = wx.grid.GridCellTextEditor()
 
193
        return renderer, editor
 
194
 
 
195
    def GetWidthPrecision(self, col_idx):
 
196
        """
 
197
        Returns width, precision.
 
198
        """
 
199
        # the col tuple should be label, type, width, precision
 
200
        try:
 
201
            width = self.col_dets[col_idx][2]
 
202
        except Exception:
 
203
            width = 5
 
204
        try:
 
205
            precision = self.col_dets[col_idx][3]
 
206
        except Exception:
 
207
            precision = 1
 
208
        return width, precision
 
209
 
 
210
    def OnLabelClick(self, event):
 
211
        "Need to give grid the focus so can process keystrokes e.g. delete"
 
212
        self.grid.SetFocus()
 
213
        event.Skip()
 
214
 
 
215
    def OnKeyDown(self, event):
 
216
        """
 
217
        http://wiki.wxpython.org/AnotherTutorial#head-999ff1e3fbf5694a51a91cf4ed2140f692da013c
 
218
        """
 
219
        if event.GetKeyCode() in [wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE]:
 
220
            self.TryToDeleteRow()
 
221
        else:
 
222
            event.Skip()
 
223
 
 
224
    def TryToDeleteRow(self):
 
225
        """
 
226
        Delete row if a row selected and not the data entry row
 
227
            and put focus on new line.
 
228
        """
 
229
        selected_rows = self.grid.GetSelectedRows()
 
230
        if len(selected_rows) == 1:
 
231
            row = selected_rows[0]
 
232
            if row != self.rows_n:
 
233
                self.grid.DeleteRows(pos=row, numRows=1)
 
234
                self.rows_n -= 1
 
235
                self.grid.SetRowLabelValue(self.rows_n, "*")
 
236
                self.grid.SetGridCursor(self.rows_n, 0)
 
237
                self.grid.HideCellEditControl()
 
238
                self.grid.ForceRefresh()
 
239
                self.SafeLayoutAdjustment()
 
240
    
 
241
    def EditorShown(self, event):
 
242
        # disable resizing until finished
 
243
        self.grid.DisableDragColSize()
 
244
        self.grid.DisableDragRowSize()
 
245
        if event.GetRow() == self.rows_n:
 
246
            self.new_editor_shown = True
 
247
        event.Skip()
 
248
        
 
249
    def EditorHidden(self, event):
 
250
        # re-enable resizing
 
251
        self.grid.EnableDragColSize()
 
252
        self.grid.EnableDragRowSize()
 
253
        self.new_editor_shown = False
 
254
        event.Skip()
 
255
        
 
256
    def OnSelectCell(self, event):
 
257
        "Store value in case we need to undo"
 
258
        row = event.GetRow()
 
259
        col = event.GetCol()
 
260
        try:
 
261
            prev_val = self.grid.GetCellValue(row, col)
 
262
            prev_tup = ((row, col), prev_val)
 
263
            self.prev_vals.append(prev_tup)
 
264
        except Exception, e:
 
265
            pass
 
266
        if len(self.prev_vals) > 2: # only need two to manage undos
 
267
            self.prev_vals.pop(0)
 
268
        new_entry = (row == self.rows_n)
 
269
        if new_entry:
 
270
            if (self.new_editor_shown or self.RowHasData(row)):
 
271
                self.grid.SetRowLabelValue(self.rows_n, "...")
 
272
            else:
 
273
                self.grid.SetRowLabelValue(self.rows_n, "*")
 
274
        event.Skip() # continue - I'm only being a man-in-the-middle ;-)
 
275
 
 
276
    def OnCellChange(self, event):
 
277
        row = event.GetRow()
 
278
        col = event.GetCol()
 
279
        new_entry = (row == self.rows_n) # if 3 rows (0,1,2) 
 
280
            # row 3 will be the new entry one
 
281
        if new_entry:
 
282
            if self.ValidRow(row=row):
 
283
                self.AddRow(row)
 
284
                event.Skip()
 
285
            else:
 
286
                self.grid.SetRowLabelValue(self.rows_n, "...")
 
287
                #print "Can't save new record - not valid"
 
288
                self.SafeLayoutAdjustment()
 
289
                event.Skip()
 
290
        else:
 
291
            if not self.ValidRow(row=row):
 
292
                #print "Invalid row"
 
293
                self.UndoCell(row, col)
 
294
            else:
 
295
                self.SafeLayoutAdjustment()
 
296
    
 
297
    def AddRow(self, row):
 
298
        ""
 
299
        # change label from * and add a new entry row on end of grid
 
300
        self.grid.AppendRows()
 
301
        # set up cell rendering and editing
 
302
        for col_idx in range(self.cols_n):
 
303
            renderer, editor = self.GetNewRendererEditor(col_idx)
 
304
            self.grid.SetCellRenderer(row + 1, col_idx, renderer)
 
305
            self.grid.SetCellEditor(row + 1, col_idx, editor)
 
306
        self.grid.SetRowLabelValue(self.rows_n, str(self.rows_n + 1))
 
307
        self.rows_n += 1
 
308
        self.grid.SetRowLabelValue(self.rows_n, "*")
 
309
        self.SafeLayoutAdjustment()
 
310
        # jump to first cell
 
311
        self.grid.SetGridCursor(self.rows_n, 0)
 
312
        # ensure we scroll there completely
 
313
        self.grid.MakeCellVisible(self.rows_n, 0)
 
314
        
 
315
    def SafeLayoutAdjustment(self):
 
316
        """
 
317
        Uses CallAfter to avoid infinite recursion.
 
318
        http://lists.wxwidgets.org/pipermail/wxpython-users/2007-April/063536.html
 
319
        """
 
320
        wx.CallAfter(self.RunSafeLayoutAdjustment)
 
321
    
 
322
    def RunSafeLayoutAdjustment(self):
 
323
        for col_idx in range(len(self.col_dets)):
 
324
            current_width = self.grid.GetColSize(col_idx)
 
325
            # identify optimal width according to content
 
326
            self.grid.AutoSizeColumn(col_idx, setAsMin=False)
 
327
            new_width = self.grid.GetColSize(col_idx)
 
328
            if new_width < current_width: # only the user can shrink a column
 
329
                # restore to current size
 
330
                self.grid.SetColSize(col_idx, current_width)
 
331
        # otherwise will only see effect after resizing
 
332
        self.grid.ForceRefresh()
 
333
    
 
334
    def UndoCell(self, row, col):
 
335
        """
 
336
        Restore original value.
 
337
        If more than one, this will be because we have selected another 
 
338
            cell while editing another.  The select event fires first adding 
 
339
            another item to prev_vals.
 
340
        NB not activated on number and float formatted cells.  These will 
 
341
            default to zero if you try to save '' to them.
 
342
        """
 
343
        for (prev_row, prev_col), prev_val in self.prev_vals:
 
344
            if prev_row == row and prev_col == col:
 
345
                self.grid.SetCellValue(row, col, prev_val)
 
346
                break
 
347
 
 
348
    def ValidRow(self, row):
 
349
        "Is row valid?"
 
350
        row_complete = True
 
351
        for i in range(self.cols_n):
 
352
            cell_val = self.grid.GetCellValue(row=row, col=i)
 
353
            if not cell_val:
 
354
                row_complete = False
 
355
                break
 
356
        return row_complete
 
357
 
 
358
    def RowHasData(self, row):
 
359
        """
 
360
        Has the row got any data stored yet?
 
361
        NB data won't be picked up if you are in the middle of entering 
 
362
            it.
 
363
        """
 
364
        has_data = False
 
365
        for i in range(self.cols_n):
 
366
            cell_val = self.grid.GetCellValue(row=row, col=i)
 
367
            if cell_val:
 
368
                has_data = True
 
369
                break
 
370
        return has_data
 
371
 
 
372
    def UpdateNewGridData(self):
 
373
        """
 
374
        Update new_grid_data.  Separated for reuse.
 
375
        """
 
376
        # get data from grid (except for final row (either empty or not saved)
 
377
        for i in range(self.rows_n):
 
378
            row_data = []
 
379
            for j, col_type in enumerate(self.col_dets):
 
380
                val = self.grid.GetCellValue(row=i, col=j)
 
381
                row_data.append(val)
 
382
            self.new_grid_data.append(tuple(row_data))
 
383
 
 
384
 
 
385
if __name__ == "__main__":
 
386
    app = wx.PySimpleApp()
 
387
    data = [(1, "Auckland"), (2, "Wellington"), (3, "Hamilton"), 
 
388
            (4, "Waiuku"), (5, "Tuakau"), (6, "Pukekohe"), (7, "Orewa"),]
 
389
    #        (8, "The People's Republic of China"), 
 
390
    #        (9, "The Democratic Republic of Congo")]
 
391
    new_grid_data = []
 
392
    # For text_browse - col label, type, width, file phrase, wildcard
 
393
    # example wildcard: "BMP files (*.bmp)|*.bmp|GIF files (*.gif)|*.gif"
 
394
    col_dets = [("Value", COL_INT, 1, 20), 
 
395
                ("Path", COL_TEXT_BROWSE, 400, "Choose a SOFA label file", 
 
396
                 "SOFA label files (*.lbl)|*.lbl")]
 
397
    grid_size = (550, 350)
 
398
    dlg = TableEntryDlg("Settings", grid_size, col_dets, data, new_grid_data)
 
399
    dlg.Show()
 
400
    app.MainLoop()
 
401
    pprint.pprint(new_grid_data)
 
402
 
 
403