Ticket #1140: gGridEditors.py

File gGridEditors.py, 21.2 kB (added by gary, 4 years ago)

First Release of a Grid DropdownList? editor and renderer. Includes a A datetextbox editor from 1138

Line 
1 """Grid Editors
2
3 20080131 - GNT
4 """
5
6 import wx
7 import dabo
8 import datetime
9 import time
10 import locale
11 import sys
12 dabo.ui.loadUI("wx")
13 #Todo: I still haven't got the Tool Tip showing for the control.
14
15 # I'm unsure of how to import this as dabo.ui.dDateTextBox.CalPanel is not available.  Coppied and Pasted here, with some minor changes:
16 class CalPanel(dabo.ui.dPanel):
17     def __init__(self, parent, pos=None, dt=None, ctrl=None ):
18         if dt is None:
19             self.date = datetime.date.today()
20         else:
21             self.date = dt
22         self.ctrl = ctrl
23         super(CalPanel, self).__init__(parent, pos=pos)
24        
25    
26     def afterInit(self):
27         """ Create the calendar control, and resize this panel
28         to the calendar's size.
29         """
30         self.cal = dabo.ui.dCalendar(self, Position=(5, 5))
31         self.cal.Date = self.date
32         self.cal.bindEvent(dabo.dEvents.Hit, self.onCalSelection)
33         self.cal.bindEvent(dabo.dEvents.KeyChar, self.onCalKey)
34         wd, ht = self.cal.Size
35         self.Size = (wd+10, ht+10)
36         self.BackColor = (192, 192, 0)
37         self.cal.Visible = True
38        
39        
40     def onCalSelection(self, evt):
41         if self.ctrl is not None:
42             self.ctrl.setDate(self.cal.Date)
43             self.ctrl.setFocus()
44         self.Visible = False
45    
46    
47     def onCalKey(self, evt):
48         if evt.keyCode == wx.WXK_ESCAPE:
49             evt.Continue = False
50             if self.ctrl is not None:
51                 self.ctrl.setFocus()
52             self.Visible = False
53
54
55
56 class GridDateTextBox(dabo.ui.dDateTextBox):
57         def showCalendar(self):
58         if self.ReadOnly:
59             # ignore
60             return
61         availHt = self.Parent.Parent.Bottom - self.Bottom
62         try:
63             self.calPanel.cal.Date = self.Value
64         except:
65             self.calPanel = CalPanel(self.Parent, dt=self.Value, ctrl=self)
66         cp = self.calPanel
67         cp.Position = (self.Left, self.Bottom)
68         if self.Bottom + cp.Height > self.Parent.Parent.Bottom:
69             # Maybe we should move it above
70             if cp.Height <= self.Top:
71                 cp.Bottom = self.Top
72             else:
73                 # We can't fit it cleanly, so try to fit as much as possible
74                 cp.Top = max(0, (self.Parent.Parent.Height - cp.Height) )
75         if self.Left + cp.Width > self.Parent.Parent.Right:
76             # Try moving it to the left
77             cp.Left = max(0, (self.Parent.Parent.Width - cp.Width) )
78         cp.Visible = True
79         cp.bringToFront()
80         # Commented out so that the grid control will receive the update from the Calendar
81         # For some reaons the application crashes when the Cal losses focus and the focus
82         #  doesn't return to the control.  I might need to execute this line but pass to the
83         #  control the "End Edit" function from the GridDataEditor.
84         #  Now it appears this doesn't work at all? Correction:  Seems to die when editing
85         #  a bizobj DataSource?  It turns out the crash is just once the control is selected...
86         # Crash is a segfault within wx libraries
87         #cp.setFocus()
88        
89     def initEvents(self):
90         self.bindEvent(dabo.dEvents.KeyChar, self.__onChar)
91         self.bindEvent(dabo.dEvents.LostFocus, self.__onLostFocus)
92         self.bindEvent(dabo.dEvents.MouseLeftDoubleClick, self.__onDblClick)
93    
94    
95     def __onDblClick(self, evt):
96         """ Display a calendar to allow users to select dates."""
97         self.showCalendar()
98        
99        
100     def __onChar(self, evt):
101         """ If a shortcut key was pressed, process that. Otherwise, eat
102         inappropriate characters.
103         """
104         try:
105             key = evt.keyChar.lower()
106             ctrl = evt.controlDown
107             shift = evt.shiftDown
108            
109             if ctrl:
110                 if shift and self.Application.Platform == "GTK":
111                     # Linux reads keys differently depending on the Shift key
112                     key = {72: "h", 77: "m", 83: "s"}[evt.keyCode]
113                 else:
114                     key = {8: "h", 13: "m", 19: "s"}[evt.keyCode]
115         except:
116             # spurious key event; ignore
117             return
118        
119         shortcutKeys = "t+-mhsyrc[]"
120         dateEntryKeys = "0123456789/- :."
121         if self.ampm:
122             dateEntryKeys + "apm"
123        
124         if key in shortcutKeys and not self.ReadOnly:
125             # There is a conflict if the key, such as '-', is used in both the
126             # date formatting and as a shortcut. So let's check the text
127             # of this field to see if it is a full date or if the user is typing in
128             # a value.
129             adjust = True
130             val = self.Value
131            
132             if val:
133                     if isinstance(val, str) or isinstance(val, unicode):
134                         valDt = self.strToDate(str(val), testing=True)
135                     else:
136                             valDt = val
137                            
138                 if valDt is None:
139                     adjust = False
140                 else:
141                     # They've just finished typing a new date, or are just
142                     # positioned on the field. Either way, update the stored
143                     # date to make sure they are in sync.
144                     self.Value = valDt
145                     evt.Continue = False
146                    
147                 if adjust:
148                     self.adjustDate(key, ctrl, shift)
149    
150         elif key in dateEntryKeys:
151             # key can be used for date entry: allow
152             pass
153         elif evt.keyCode in range(32, 129):
154             # key is in ascii range, but isn't one of the above
155             # allowed key sets. Disallow.
156             evt.stop()
157         else:
158             # Pass the key up the chain to process - perhaps a Tab, Enter, or Backspace...
159             pass
160
161    
162     def __onLostFocus(self, evt):
163         val = self.Value
164         try:
165             newVal = self.strToDate(self.GetValue())
166             if newVal != val:
167                 self.Value = newVal
168         except: pass
169        
170 class GridDateEditor(wx.grid.PyGridCellEditor):
171     def __init__(self, *args, **kwargs):
172                 super(GridDateEditor, self).__init__(*args, **kwargs)
173
174     def Create(self, parent, id, evtHandler, *args, **kwargs):
175         control = GridDateTextBox(parent=parent, id=id)
176         self.control = control
177         self.SetControl(self.control)
178         if evtHandler:
179             self.control.PushEventHandler(evtHandler)
180         #super(GridDateEditor, self).Create(parent, id, evtHandler)
181        
182
183     def Clone(self):
184         return self.__class__()
185
186     def SetParameters(self, paramStr):
187         dabo.infoLog.write("GridDateEditor: SetParameters: %s" % paramStr)
188         #self.control.Choices = eval(paramStr)
189
190     def BeginEdit(self, row, col, grid):
191         self.value = grid.GetTable().GetValue(row, col)
192         update = True
193                 #datetime.datetime.fromtimestamp(time.mktime(time.strptime(mytime, time_format)))
194                
195         try:
196                 try:
197                         timeValue = time.strptime(self.value, self.DateFormat)
198                     except ValueError:
199                             #Take our best Guess
200                             dabo.infoLog.write("GridDateEditor: Guessing Time Format")
201                             try:
202                                     timeValue = self.control.strToDate(self.value).timetuple()
203                             except:
204                                     # Still No Luck, default to today
205                                     update = dabo.ui.areYouSure(title='Error in Existing Date Format', message='Do you want to reset this date field?',
206                                                              cancelButton=False)
207                                         timeValue = datetime.date.today().timetuple()
208                    
209                     if update:     
210                     dateValue = datetime.date(timeValue[0], timeValue[1], timeValue[2])
211                     self.control.Value = dateValue
212                     #self.control.Value = self.value
213
214         except ValueError, vError:
215             dabo.infoLog.write("GridDateEditor: ValueError in BeginEdit: " + str(vError))
216
217                 if not update:
218                         self.Reset()
219                    
220         self.control.SetFocus()
221
222     def EndEdit(self, row, col, grid):
223         changed = False
224         v = self.control.Value.strftime(self.DateFormat)
225         #v = self.control.Value
226        
227         if v != self.value:
228             changed = True
229         if changed:
230             grid.GetTable().SetValue(row, col, v)
231             # I don't think the DateTextBox handles an empty value?
232         #self.value = ""
233         #self.control.Value = self.value
234         return changed
235
236     def Reset(self):
237         self.control.Value = self.value
238
239     def SetSize(self, rectorig):
240         rect = rectorig
241         self.control.SetDimensions(rect.x+3, rect.y+3, rect.width-2, rect.height-2)
242
243     def IsAcceptedKey(self, key):
244         return True
245
246         #Properties
247     def _getDateFormat(self):
248                 # Get the date format - Make it up if none is given using the system locale
249                 if hasattr(self, "_dateFormat"):
250                 return self._dateFormat
251         else:
252                         #dateFormat = locale.nl_langinfo(locale.D_FMT) # %d/%m/%y
253                         #pos = dateFormat.find("%")+1
254                         #date1 = dateFormat[pos]
255                         #pos = dateFormat.find("%", pos)+1
256                         #date2 = dateFormat[pos]
257                         #pos = dateFormat.find("%", pos)+1
258                         #date3 = dateFormat[pos]
259                         #return "%" + date1 + "-%" + date2 + "-%" + date3
260                         return "%Y-%m-%d"
261        
262     def _setDateFormat(self, val):
263             #Todo: Check format is valid
264         self._dateFormat = val
265     DateFormat = property(_getDateFormat, _setDateFormat, None,
266             "Get or Set the Date Format String for this Control.")
267
268
269 #gbd info:
270 #Program received signal SIGSEGV, Segmentation fault.
271 #[Switching to Thread 0xb7c9b6c0 (LWP 6096)]
272 #---Type <return> to continue, or q <return> to quit---
273 #0xb733a91e in wxGridCellEditor::Show () from /usr/lib/libwx_gtk2u_adv-2.8.so.0
274
275
276 # There is an issue with this renderer class where a black highlight remains around changed cells.  This is caused
277 # purely by the use of this renderer.
278
279 # TODO: If this becomes included within dabo, it is recommended to remove the need for
280 # getKeys, getChoices, getValueMode and set these arguements in the dGrid._updateEditor function as with the existing List Editor
281 class gGridDropdownListRenderer(wx.grid.PyGridCellRenderer):
282         """Display the Lookup value of a combo or list box instead of the actual value.
283         You must inherit this class and override the getKeys, getChoices and getValueMode Functions."""
284         def __init__(self, *args, **kwargs):
285         self.__valueLookupDict = self.getLookupDict()
286                 super(gGridDropdownListRenderer, self).__init__(*args, **kwargs)
287
288         # These functions must be overriden
289         def getKeys(self):
290                 """Override this function to return a list of stored keys stored in this datafield."""
291                 return [2, 4, 6, 8]
292         def getChoices(self):
293                 """Override this function to return a list of values to be displayed for this datafield."""
294                 return ["The Ants", "Go Marching", "Two by Two", "Hoorah, Hoorah"]
295         def getValueMode(self):
296                 """Override this Function to return the ValueMode for the Combo or List box."""
297                 return 'Key'
298                
299         def getLookupDict(self):
300                 """Override this function to return a dictionary of values to display.  The dictionary key is the stored value."""
301                 if self.getValueMode() == 'Key':
302                         thisDict = {}
303                         keys = self.getKeys()
304                         choices = self.getChoices()
305                         for i in range(len(keys)):
306                                 thisDict[keys[i]] = choices[i]
307                         return thisDict
308                
309         # This is the function overriden from the parent class.
310     def Draw(self, grid, attr, dc, rect, row, col, isSelected):
311         """Customisation Point: Draw the data from grid in the rectangle with attributes using the dc"""
312         self.clip(dc, rect)
313         # We use our custom attr, not the one wx passes:
314         attr = grid._Table.GetAttr(row, col)
315         try:
316             txt = self.getValueText(grid, row, col)
317                 dc.Clear()
318                 super(gGridDropdownListRenderer, self).Draw(grid, attr, dc, rect, row, col, isSelected)
319             self.drawText(txt, attr, dc, rect)
320             #super(gGridDropdownListRenderer, self).Draw(grid, attr, dc, rect, row, col, isSelected)
321         finally:
322             self.unclip(dc)
323                         pass
324                
325                
326     def getValueText( self, grid, row, col ):
327             """Returns the text to display in the grid.  If the key does not exist, then the
328             key will be displayed."""
329             value = grid._Table.GetValue(row, col)
330             if value in self.__valueLookupDict:
331                 return self.__valueLookupDict[value]
332         else:
333                 return value
334
335     def drawText( self, txt, attr, dc, rect):
336         dc.DrawText(txt, rect.x, rect.y)
337        
338
339     def clip( self, dc, rect ):
340         """Setup the clipping rectangle"""
341         dc.SetClippingRegion( rect.x, rect.y, rect.width, rect.height )
342     def unclip( self, dc ):
343         """Destroy the clipping rectangle"""
344         dc.DestroyClippingRegion( )
345
346 class gGridDropdownListDebug(dabo.ui.dDropdownList):
347     def onDestroy(self, evt):
348             dabo.infoLog.write("gGridDropdownList: OnDestroy")
349             #super(gGridDropdownListEditor, self).__init__(*args, **kwargs)
350     def onCreate(self, evt):
351             dabo.infoLog.write("gGridDropdownList: OnCreate ")
352     def onMouseEvent(self, evt):
353             dabo.infoLog.write("gGridDropdownList: MouseEvent ")
354     def onGotFocus(self, evt):
355             dabo.infoLog.write("gGridDropdownList: GotFocus")
356     def onLostFocus(self, evt):
357             dabo.infoLog.write("gGridDropdownList: LostFocus")
358     def onHit(self, evt):
359             dabo.infoLog.write("gGridDropdownList: Hit")
360             evt.Continue = True
361                 #print "New Key, ", evt.EventData['index']
362                 #print "New Choice, ", evt.EventObject
363                 #evt.EventObject.SetFocus()
364                 args = self.endEditArgs
365                 kwargs = self.endEditKwargs
366                 self.endEdit(*args, **kwargs)
367
368     def onValueChanged(self, evt):
369             dabo.infoLog.write("gGridDropdownList: ValueChanged")
370            
371     def setEndEdit(self, endEdit, *args, **kwargs):
372             self.endEdit = endEdit
373             self.endEditArgs = args
374             self.endEditKwargs = kwargs
375
376 class gGridDropdownList(dabo.ui.dDropdownList):
377         """Due to an issue with PyGridCellEditor calling EndEdit on control lostFocus (and the DropdownList/wx.Choice looses focus a little early),
378 this class has been created with the intention of still capturing user input."""
379         # I suspect that wx.grid.GridCellChoiceEditor is designed specifically not to loose focus during editing.  It might be worth overriding
380         # some of the functionality of dDropdownList / wx.Choice in a new subclass to behave in a similiar manner.
381     def onHit(self, evt):
382                 args = self.endEditArgs
383                 kwargs = self.endEditKwargs
384                 self.endEdit(*args, **kwargs)
385
386     def setEndEdit(self, endEdit, *args, **kwargs):
387             self.endEdit = endEdit
388             self.endEditArgs = args
389             self.endEditKwargs = kwargs
390                        
391        
392        
393 # TODO: If this becomes included within dabo, it is recommended to remove the need for
394 # getKeys, getChoices, getValueMode and set these arguements in the dGrid._updateEditor function as with the existing List Editor
395 class gGridDropdownListEditor(wx.grid.PyGridCellEditor):
396         """GridComboListEditor embeds a (dabo) combo box or (dabo) list box within a (dabo) grid.
397         This class must be inherited and have the getKeys, getChoices and getValueMode methods overriden."""
398
399     def __init__(self, *args, **kwargs):
400                 # This is to allow experimentation with combo boxes as well
401         self.controlClass = gGridDropdownList
402         #This must be called for reasons I don't understand, and called upon this class.
403                 super(gGridDropdownListEditor, self).__init__(*args, **kwargs)
404                
405         # This function must be overriden
406         def getKeys(self):
407                 """Override this function to return a list of stored keys stored in this datafield."""
408                 return [2, 4, 6, 8]
409         def getChoices(self):
410                 """Override this function to return a list of values to be displayed for this datafield."""
411                 return ["The Ants", "Go Marching", "Two by Two", "Hoorah, Hoorah"]
412         def getValueMode(self):
413                 """Override this Function to return the ValueMode for the Combo or List box."""
414                 return 'Key'
415        
416     def Create(self, parent, id, evtHandler, *args, **kwargs):
417         control = self.controlClass(parent=parent, id=id, Keys=self.getKeys(), Choices=self.getChoices(), ValueMode=self.getValueMode())
418         self.control = control
419         self.SetControl(self.control)
420         if evtHandler:
421             self.control.PushEventHandler(evtHandler)
422         # This Control does not appear to be receiving mouse events properly... I had this problem with the DateTextBox
423         # Once the drop down list is selected, the control looses focus and then the grid will no longer be updated.
424         # This is because EndEdit is called after the object has lost focus (I think). Quick Fix is to catch event in
425         # above class (gGridDropdownList)
426         super(gGridDropdownListEditor, self).Create(parent, id, evtHandler)
427        
428     def Clone(self):
429         return self.__class__()
430
431     def SetParameters(self, paramStr):
432         dabo.infoLog.write("gGridDropdownListEditor: SetParameters: %s" % paramStr)
433         #self.control.Choices = eval(paramStr)
434
435     def BeginEdit(self, row, col, grid):
436         self.value = grid.GetTable().GetValue(row, col)
437         try:
438                 if self.value in self.control.Keys:
439                     self.control.Value = self.value
440         except ValueError:
441             #dabo.infoLog.write("gGridDropdownListEditor: Value not in Choices")
442             pass
443        
444         self.control.setEndEdit(self.EndEdit, row, col, grid)
445         self.control.SetFocus()
446
447     def EndEdit(self, row, col, grid):
448         changed = False
449         v = self.control.Value
450         if v != self.value and v is not None:
451             changed = True
452         if changed:
453             grid.GetTable().SetValue(row, col, v)
454             self.value = ""
455         self.control.Value = self.value
456         return changed
457        
458 #   def SetSize(self, rectorig):
459 #       dabo.infoLog.write("GridListEditor: SetSize: %s" % rectorig)
460 #       dabo.infoLog.write("GridListEditor: type of rectorig: %s" % type(rectorig))
461 #       rect = wx.Rect(rectorig.X, rectorig.Y, rectorig.Width+10, rectorig.Height+10)
462 #       #rect = rectorig
463 #       dabo.infoLog.write("GridListEditor RECT: %s" % rect)
464 #       super(gGridDropdownListEditor, self).SetSize(rect)
465 #       #self.control.fitToSizer()
466
467     def Reset(self):
468         self.control.Value = self.value
469     def IsAcceptedKey(self, key):
470         return True
471
472 class _dGrid_test(dabo.ui.dGrid):
473     def initProperties(self):
474         self.DataSet = [
475                 {"name" : "Ed Leafe", "age" : 49, "coder" :  True, "color": "2008-10-10"},
476                 {"name" : "Paul McNett", "age" : 37, "coder" :  True, "color": "11-10-2008"},
477                 {"name" : "Ted Roche", "age" : 48, "coder" :  True, "color": "2008-10-12"},
478                 {"name" : "Derek Jeter", "age": 32 , "coder" :  False, "color": "white"},
479                 {"name" : "One", "age" : 38, "coder" :  False, "color": "orange"},
480                 {"name" : "Steve Wozniak", "age" : 56, "coder" :  True, "color": "yellow"},
481                 {"name" : 6, "age" : 22, "coder" :  False, "color": "gold"},
482                 {"name" : "Madeline Albright", "age" : 69, "coder" :  False, "color": "red"}]
483         self.Width = 360
484         self.Height = 150
485         self.Editable = True
486         #self.Sortable = False
487         #self.Searchable = False
488
489
490     def afterInit(self):
491         self.super()
492
493         self.addColumn(Name="Geek", DataField="coder", Caption="Geek?",
494                 Order=10, DataType="bool", Width=60, Sortable=False,
495                 Searchable=False, Editable=True, HeaderFontBold=False)
496
497         col = dabo.ui.dColumn(self, Name="Person", Order=20, DataField="name",
498                 DataType="string", Width=200, Caption="Celebrity Name",
499                 Sortable=True, Searchable=True, Editable=True, Expand=False)
500         self.addColumn(col)
501
502         col.HeaderFontItalic = True
503         col.HeaderBackColor = "orange"
504         col.HeaderVerticalAlignment = "Top"
505         col.HeaderHorizontalAlignment = "Left"
506         #col.ListEditorChoices = dabo.dColors.colors
507         #col.CustomEditorClass = GridListEditor
508
509         col.CustomEditorClass = gGridDropdownListEditor
510         col.CustomRendererClass = gGridDropdownListRenderer
511         #col.CustomRendererClass = wx.grid.GridCellStringRenderer
512
513         self.addColumn(Name="Age", Order=30, DataField="age",
514                 DataType="integer", Width=40, Caption="Age",
515                 Sortable=True, Searchable=True, Editable=True)
516
517         col = dabo.ui.dColumn(self, Name="Color", Order=40, DataField="color",
518                 DataType="string", Width=40, Caption="Favorite Color",
519                 Sortable=True, Searchable=True, Editable=True, Expand=False)
520         self.addColumn(col)
521
522         #col.ListEditorChoices = dabo.dColors.colors
523         #col.CustomEditorClass = gGridDropdownListEditor
524         #col.CustomEditorClass = GridListEditor
525         col.CustomEditorClass = GridDateEditor
526
527         col.HeaderVerticalAlignment = "Bottom"
528         col.HeaderHorizontalAlignment = "Right"
529         col.HeaderForeColor = "brown"
530
531         self.RowLabels = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
532         #self.ShowRowLabels = True
533
534 if __name__ == '__main__':
535     class TestForm(dabo.ui.dForm):
536         def afterInit(self):
537             self.BackColor = "khaki"
538             g = self.grid = _dGrid_test(self, RegID="sampleGrid")
539             self.Sizer.append(g, 1, "x", border=40, borderSides="all")
540             self.Sizer.appendSpacer(10)
541             gsz = dabo.ui.dGridSizer(HGap=50)
542
543             chk = dabo.ui.dCheckBox(self, Caption="Edit Table", RegID="geekEdit",
544                     DataSource="sampleGrid", DataField="Editable")
545             chk.refresh()
546             gsz.append(chk, row=0, col=0)
547
548             chk = dabo.ui.dCheckBox(self, Caption="Show Row Labels",
549                     RegID="showRowLabels", DataSource="sampleGrid",
550                     DataField="ShowRowLabels")
551             gsz.append(chk, row=1, col=0)
552             chk.refresh()
553
554             chk = dabo.ui.dCheckBox(self, Caption="Allow Multiple Selection",
555                     RegID="multiSelect", DataSource="sampleGrid",
556                     DataField="MultipleSelection")
557             chk.refresh()
558             gsz.append(chk, row=2, col=0)
559
560             radSelect = dabo.ui.dRadioList(self, Choices=["Row", "Col", "Cell"],
561                     ValueMode="string", Caption="Sel Mode", BackColor=self.BackColor,
562                     DataSource="sampleGrid", DataField="SelectionMode", RegID="radSelect")
563             radSelect.refresh()
564             gsz.append(radSelect, row=0, col=1, rowSpan=3)
565
566             self.Sizer.append(gsz, halign="Center", border=10)
567             gsz.setColExpand(True, 1)
568             self.layout()
569
570             self.fitToSizer(20,20)
571
572
573     app = dabo.dApp(MainFormClass=TestForm)
574     app.setup()
575     app.MainForm.radSelect.setFocus()
576     app.start()
577    
578 # GridCellChoice