9686e4bf58482e094c1277782ab653c105e331cc
[hooke.git] / hooke / ui / gui / panel / propertyeditor-propgrid.py
1 # Copyright (C) 2010 Massimo Sandal <devicerandom@gmail.com>
2 #                    W. Trevor King <wking@drexel.edu>
3 #
4 # This file is part of Hooke.
5 #
6 # Hooke is free software: you can redistribute it and/or modify it
7 # under the terms of the GNU Lesser General Public License as
8 # published by the Free Software Foundation, either version 3 of the
9 # License, or (at your option) any later version.
10 #
11 # Hooke is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
14 # Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with Hooke.  If not, see
18 # <http://www.gnu.org/licenses/>.
19
20 """Property editor panel for Hooke.
21 """
22
23 import sys
24 import os.path
25
26 import wx
27 import wx.propgrid as wxpg
28
29 # There are many comments and code fragments in here from the demo app.
30 # They should come in handy to expand the functionality in the future.
31
32 class Display (object):
33     property_descriptor = []
34     def __init__(self):
35         pass
36
37 class ValueObject (object):
38     def __init__(self):
39         pass
40
41
42 class IntProperty2 (wxpg.PyProperty):
43     """This is a simple re-implementation of wxIntProperty.
44     """
45     def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=0):
46         wxpg.PyProperty.__init__(self, label, name)
47         self.SetValue(value)
48
49     def GetClassName(self):
50         return "IntProperty2"
51
52     def GetEditor(self):
53         return "TextCtrl"
54
55     def GetValueAsString(self, flags):
56         return str(self.GetValue())
57
58     def PyStringToValue(self, s, flags):
59         try:
60             v = int(s)
61             if self.GetValue() != v:
62                 return v
63         except TypeError:
64             if flags & wxpg.PG_REPORT_ERROR:
65                 wx.MessageBox("Cannot convert '%s' into a number."%s, "Error")
66         return False
67
68     def PyIntToValue(self, v, flags):
69         if (self.GetValue() != v):
70             return v
71
72
73 class PyFilesProperty(wxpg.PyArrayStringProperty):
74     def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=[]):
75         wxpg.PyArrayStringProperty.__init__(self, label, name, value)
76         self.SetValue(value)
77
78     def OnSetValue(self, v):
79         self.value = v
80         self.display = ', '.join(self.value)
81
82     def GetValueAsString(self, argFlags):
83         return self.display
84
85     def PyStringToValue(self, s, flags):
86         return [a.strip() for a in s.split(',')]
87
88     def OnEvent(self, propgrid, ctrl, event):
89         if event.GetEventType() == wx.wxEVT_COMMAND_BUTTON_CLICKED:
90             # Show dialog to select a string, call DoSetValue and
91             # return True, if value changed.
92             return True
93
94         return False
95
96
97 class PyObjectPropertyValue:
98     """\
99     Value type of our sample PyObjectProperty. We keep a simple dash-delimited
100     list of string given as argument to constructor.
101     """
102     def __init__(self, s=None):
103         try:
104             self.ls = [a.strip() for a in s.split('-')]
105         except:
106             self.ls = []
107
108     def __repr__(self):
109         return ' - '.join(self.ls)
110
111
112 class PyObjectProperty(wxpg.PyProperty):
113     """\
114     Another simple example. This time our value is a PyObject (NOTE: we can't
115     return an arbitrary python object in DoGetValue. It cannot be a simple
116     type such as int, bool, double, or string, nor an array or wxObject based.
117     Dictionary, None, or any user-specified Python object is allowed).
118     """
119     def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=None):
120         wxpg.PyProperty.__init__(self, label, name)
121         self.SetValue(value)
122
123     def GetClassName(self):
124         return self.__class__.__name__
125
126     def GetEditor(self):
127         return "TextCtrl"
128
129     def GetValueAsString(self, flags):
130         return repr(self.GetValue())
131
132     def PyStringToValue(self, s, flags):
133         return PyObjectPropertyValue(s)
134
135
136 class ShapeProperty(wxpg.PyEnumProperty):
137     """\
138     Demonstrates use of OnCustomPaint method.
139     """
140     def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=-1):
141         wxpg.PyEnumProperty.__init__(self, label, name, ['Line','Circle','Rectangle'], [0,1,2], value)
142
143     def OnMeasureImage(self, index):
144         return wxpg.DEFAULT_IMAGE_SIZE
145
146     def OnCustomPaint(self, dc, rect, paint_data):
147         """\
148         paint_data.m_choiceItem is -1 if we are painting the control,
149         in which case we need to get the drawn item using DoGetValue.
150         """
151         item = paint_data.m_choiceItem
152         if item == -1:
153             item = self.DoGetValue()
154
155         dc.SetPen(wx.Pen(wx.BLACK))
156         dc.SetBrush(wx.Brush(wx.BLACK))
157
158         if item == 0:
159             dc.DrawLine(rect.x,rect.y,rect.x+rect.width,rect.y+rect.height)
160         elif item == 1:
161             half_width = rect.width / 2
162             dc.DrawCircle(rect.x+half_width,rect.y+half_width,half_width-3)
163         elif item == 2:
164             dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
165
166
167 class LargeImagePickerCtrl(wx.Window):
168     """\
169     Control created and used by LargeImageEditor.
170     """
171     def __init__(self):
172         pre = wx.PreWindow()
173         self.PostCreate(pre)
174
175     def Create(self, parent, id_, pos, size, style = 0):
176         wx.Window.Create(self, parent, id_, pos, size, style | wx.BORDER_SIMPLE)
177         img_spc = size[1]
178         self.tc = wx.TextCtrl(self, -1, "", (img_spc,0), (2048,size[1]), wx.BORDER_NONE)
179         self.SetBackgroundColour(wx.WHITE)
180         self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
181         self.property = None
182         self.bmp = None
183         self.Bind(wx.EVT_PAINT, self.OnPaint)
184
185     def OnPaint(self, event):
186         dc = wx.BufferedPaintDC(self)
187
188         whiteBrush = wx.Brush(wx.WHITE)
189         dc.SetBackground(whiteBrush)
190         dc.Clear()
191
192         bmp = self.bmp
193         if bmp:
194             dc.DrawBitmap(bmp, 2, 2)
195         else:
196             dc.SetPen(wx.Pen(wx.BLACK))
197             dc.SetBrush(whiteBrush)
198             dc.DrawRectangle(2, 2, 64, 64)
199
200     def RefreshThumbnail(self):
201         """\
202         We use here very simple image scaling code.
203         """
204         if not self.property:
205             self.bmp = None
206             return
207
208         path = self.property.DoGetValue()
209
210         if not os.path.isfile(path):
211             self.bmp = None
212             return
213
214         image = wx.Image(path)
215         image.Rescale(64, 64)
216         self.bmp = wx.BitmapFromImage(image)
217
218     def SetProperty(self, property):
219         self.property = property
220         self.tc.SetValue(property.GetDisplayedString())
221         self.RefreshThumbnail()
222
223     def SetValue(self, s):
224         self.RefreshThumbnail()
225         self.tc.SetValue(s)
226
227     def GetLastPosition(self):
228         return self.tc.GetLastPosition()
229
230
231 class LargeImageEditor(wxpg.PyEditor):
232     """\
233     Double-height text-editor with image in front.
234     """
235     def __init__(self):
236         wxpg.PyEditor.__init__(self)
237
238     def CreateControls(self, propgrid, property, pos, sz):
239         try:
240             h = 64 + 6
241             x = propgrid.GetSplitterPosition()
242             x2 = propgrid.GetClientSize().x
243             bw = propgrid.GetRowHeight()
244             lipc = LargeImagePickerCtrl()
245             if sys.platform == 'win32':
246                 lipc.Hide()
247             lipc.Create(propgrid, wxpg.PG_SUBID1, (x,pos[1]), (x2-x-bw,h))
248             lipc.SetProperty(property)
249             # Hmmm.. how to have two-stage creation without subclassing?
250             #btn = wx.PreButton()
251             #pre = wx.PreWindow()
252             #self.PostCreate(pre)
253             #if sys.platform == 'win32':
254             #    btn.Hide()
255             #btn.Create(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS)
256             btn = wx.Button(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS)
257             return (lipc, btn)
258         except:
259             import traceback
260             print traceback.print_exc()
261
262     def UpdateControl(self, property, ctrl):
263         ctrl.SetValue(property.GetDisplayedString())
264
265     def DrawValue(self, dc, property, rect):
266         if not (property.GetFlags() & wxpg.PG_PROP_AUTO_UNSPECIFIED):
267             dc.DrawText( property.GetDisplayedString(), rect.x+5, rect.y );
268
269     def OnEvent(self, propgrid, ctrl, event):
270         if not ctrl:
271             return False
272
273         evtType = event.GetEventType()
274
275         if evtType == wx.wxEVT_COMMAND_TEXT_ENTER:
276             if propgrid.IsEditorsValueModified():
277                 return True
278
279         elif evtType == wx.wxEVT_COMMAND_TEXT_UPDATED:
280             if not property.HasFlag(wxpg.PG_PROP_AUTO_UNSPECIFIED) or not ctrl or \
281                ctrl.GetLastPosition() > 0:
282
283                 # We must check this since an 'empty' text event
284                 # may be triggered when creating the property.
285                 PG_FL_IN_SELECT_PROPERTY = 0x00100000
286                 if not (propgrid.GetInternalFlags() & PG_FL_IN_SELECT_PROPERTY):
287                     event.Skip();
288                     event.SetId(propgrid.GetId());
289
290                 propgrid.EditorsValueWasModified();
291
292         return False
293
294
295     def CopyValueFromControl(self, property, ctrl):
296         tc = ctrl.tc
297         res = property.SetValueFromString(tc.GetValue(),0)
298         # Changing unspecified always causes event (returning
299         # true here should be enough to trigger it).
300         if not res and property.IsFlagSet(wxpg.PG_PROP_AUTO_UNSPECIFIED):
301             res = True
302
303         return res
304
305     def SetValueToUnspecified(self, ctrl):
306         ctrl.tc.Remove(0,len(ctrl.tc.GetValue()));
307
308     def SetControlStringValue(self, ctrl, txt):
309         ctrl.SetValue(txt)
310
311     def OnFocus(self, property, ctrl):
312         ctrl.tc.SetSelection(-1,-1)
313         ctrl.tc.SetFocus()
314
315
316 class PropertyEditor(wx.Panel):
317
318     def __init__(self, parent):
319         # Use the WANTS_CHARS style so the panel doesn't eat the Return key.
320         wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS, size=(160, 200))
321
322         sizer = wx.BoxSizer(wx.VERTICAL)
323
324         self.pg = wxpg.PropertyGrid(self, style=wxpg.PG_SPLITTER_AUTO_CENTER|wxpg.PG_AUTO_SORT)
325
326         # Show help as tooltips
327         self.pg.SetExtraStyle(wxpg.PG_EX_HELP_AS_TOOLTIPS)
328
329         #pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChange)
330         #pg.Bind(wxpg.EVT_PG_SELECTED, self.OnPropGridSelect)
331         #self.pg.Bind(wxpg.EVT_PG_RIGHT_CLICK, self.OnPropGridRightClick)
332
333         # Needed by custom image editor
334         wx.InitAllImageHandlers()
335
336         #
337         # Let's create a simple custom editor
338         #
339         # NOTE: Editor must be registered *before* adding a property that uses it.
340         self.pg.RegisterEditor(LargeImageEditor)
341
342         '''
343         #
344         # Add properties
345         #
346
347         pg.Append( wxpg.PropertyCategory("1 - Basic Properties") )
348         pg.Append( wxpg.StringProperty("String",value="Some Text") )
349         pg.Append( wxpg.IntProperty("Int",value=100) )
350         pg.Append( wxpg.FloatProperty("Float",value=100.0) )
351         pg.Append( wxpg.BoolProperty("Bool",value=True) )
352         pg.Append( wxpg.BoolProperty("Bool_with_Checkbox",value=True) )
353         pg.SetPropertyAttribute("Bool_with_Checkbox", "UseCheckbox", True)
354
355         pg.Append( wxpg.PropertyCategory("2 - More Properties") )
356         pg.Append( wxpg.LongStringProperty("LongString",value="This is a\\nmulti-line string\\nwith\\ttabs\\nmixed\\tin.") )
357         pg.Append( wxpg.DirProperty("Dir",value="C:\\Windows") )
358         pg.Append( wxpg.FileProperty("File",value="C:\\Windows\\system.ini") )
359         pg.Append( wxpg.ArrayStringProperty("ArrayString",value=['A','B','C']) )
360
361         pg.Append( wxpg.EnumProperty("Enum","Enum",
362                                      ['wxPython Rules','wxPython Rocks','wxPython Is The Best'],
363                                      [10,11,12],0) )
364         pg.Append( wxpg.EditEnumProperty("EditEnum","EditEnumProperty",['A','B','C'],[0,1,2],"Text Not in List") )
365
366         pg.Append( wxpg.PropertyCategory("3 - Advanced Properties") )
367         pg.Append( wxpg.DateProperty("Date",value=wx.DateTime_Now()) )
368         pg.Append( wxpg.FontProperty("Font",value=self.GetFont()) )
369         pg.Append( wxpg.ColourProperty("Colour",value=self.GetBackgroundColour()) )
370         pg.Append( wxpg.SystemColourProperty("SystemColour") )
371         pg.Append( wxpg.ImageFileProperty("ImageFile") )
372         pg.Append( wxpg.MultiChoiceProperty("MultiChoice",choices=['wxWidgets','QT','GTK+']) )
373
374         pg.Append( wxpg.PropertyCategory("4 - Additional Properties") )
375         pg.Append( wxpg.PointProperty("Point",value=self.GetPosition()) )
376         pg.Append( wxpg.SizeProperty("Size",value=self.GetSize()) )
377         pg.Append( wxpg.FontDataProperty("FontData") )
378         pg.Append( wxpg.IntProperty("IntWithSpin",value=256) )
379         pg.SetPropertyEditor("IntWithSpin","SpinCtrl")
380         pg.Append( wxpg.DirsProperty("Dirs",value=['C:/Lib','C:/Bin']) )
381         pg.SetPropertyHelpString( "String", "String Property help string!" )
382         pg.SetPropertyHelpString( "Dirs", "Dirs Property help string!" )
383
384         pg.SetPropertyAttribute( "File", wxpg.PG_FILE_SHOW_FULL_PATH, 0 )
385         pg.SetPropertyAttribute( "File", wxpg.PG_FILE_INITIAL_PATH, "C:\\Program Files\\Internet Explorer" )
386         pg.SetPropertyAttribute( "Date", wxpg.PG_DATE_PICKER_STYLE, wx.DP_DROPDOWN|wx.DP_SHOWCENTURY )
387
388         pg.Append( wxpg.PropertyCategory("5 - Custom Properties") )
389         pg.Append( IntProperty2("IntProperty2", value=1024) )
390
391         pg.Append( ShapeProperty("ShapeProperty", value=0) )
392         pg.Append( PyObjectProperty("PyObjectProperty") )
393
394         pg.Append( wxpg.ImageFileProperty("ImageFileWithLargeEditor") )
395         pg.SetPropertyEditor("ImageFileWithLargeEditor", "LargeImageEditor")
396
397
398         pg.SetPropertyClientData( "Point", 1234 )
399         if pg.GetPropertyClientData( "Point" ) != 1234:
400             raise ValueError("Set/GetPropertyClientData() failed")
401
402         # Test setting unicode string
403         pg.GetPropertyByName("String").SetValue(u"Some Unicode Text")
404
405         #
406         # Test some code that *should* fail (but not crash)
407         #try:
408             #a_ = pg.GetPropertyValue( "NotARealProperty" )
409             #pg.EnableProperty( "NotAtAllRealProperty", False )
410             #pg.SetPropertyHelpString( "AgaintNotARealProperty", "Dummy Help String" )
411         #except:
412             #pass
413             #raise
414
415         '''
416         sizer.Add(self.pg, 1, wx.EXPAND)
417         self.SetSizer(sizer)
418         sizer.SetSizeHints(self)
419
420         self.SelectedTreeItem = None
421
422     def GetPropertyValues(self):
423         return self.pg.GetPropertyValues()
424
425     def Initialize(self, properties):
426         pg = self.pg
427         pg.Clear()
428
429         if properties:
430             for element in properties:
431                 if element[1]['type'] == 'arraystring':
432                     elements = element[1]['elements']
433                     if 'value' in element[1]:
434                         property_value = element[1]['value']
435                     else:
436                         property_value = element[1]['default']
437                     #retrieve individual strings
438                     property_value = split(property_value, ' ')
439                     #remove " delimiters
440                     values = [value.strip('"') for value in property_value]
441                     pg.Append(wxpg.ArrayStringProperty(element[0], value=values))
442
443                 if element[1]['type'] == 'boolean':
444                     if 'value' in element[1]:
445                         property_value = element[1].as_bool('value')
446                     else:
447                         property_value = element[1].as_bool('default')
448                     property_control = wxpg.BoolProperty(element[0], value=property_value)
449                     pg.Append(property_control)
450                     pg.SetPropertyAttribute(element[0], 'UseCheckbox', True)
451
452                 #if element[0] == 'category':
453                     #pg.Append(wxpg.PropertyCategory(element[1]))
454
455                 if element[1]['type'] == 'color':
456                     if 'value' in element[1]:
457                         property_value = element[1]['value']
458                     else:
459                         property_value = element[1]['default']
460                     property_value = eval(property_value)
461                     pg.Append(wxpg.ColourProperty(element[0], value=property_value))
462
463                 if element[1]['type'] == 'enum':
464                     elements = element[1]['elements']
465                     if 'value' in element[1]:
466                         property_value = element[1]['value']
467                     else:
468                         property_value = element[1]['default']
469                     pg.Append(wxpg.EnumProperty(element[0], element[0], elements, [], elements.index(property_value)))
470
471                 if element[1]['type'] == 'filename':
472                     if 'value' in element[1]:
473                         property_value = element[1]['value']
474                     else:
475                         property_value = element[1]['default']
476                     pg.Append(wxpg.FileProperty(element[0], value=property_value))
477
478                 if element[1]['type'] == 'float':
479                     if 'value' in element[1]:
480                         property_value = element[1].as_float('value')
481                     else:
482                         property_value = element[1].as_float('default')
483                     property_control = wxpg.FloatProperty(element[0], value=property_value)
484                     pg.Append(property_control)
485
486                 if element[1]['type'] == 'folder':
487                     if 'value' in element[1]:
488                         property_value = element[1]['value']
489                     else:
490                         property_value = element[1]['default']
491                     pg.Append(wxpg.DirProperty(element[0], value=property_value))
492
493                 if element[1]['type'] == 'integer':
494                     if 'value' in element[1]:
495                         property_value = element[1].as_int('value')
496                     else:
497                         property_value = element[1].as_int('default')
498                     property_control = wxpg.IntProperty(element[0], value=property_value)
499                     if 'maximum' in element[1]:
500                         property_control.SetAttribute('Max', element[1].as_int('maximum'))
501                     if 'minimum' in element[1]:
502                         property_control.SetAttribute('Min', element[1].as_int('minimum'))
503                     property_control.SetAttribute('Wrap', True)
504                     pg.Append(property_control)
505                     pg.SetPropertyEditor(element[0], 'SpinCtrl')
506
507                 if element[1]['type'] == 'string':
508                     if 'value' in element[1]:
509                         property_value = element[1]['value']
510                     else:
511                         property_value = element[1]['default']
512                     pg.Append(wxpg.StringProperty(element[0], value=property_value))
513
514         pg.Refresh()
515
516     def OnReserved(self, event):
517         pass