3 """Property editor panel for Hooke.
\r
5 wxPropertyGrid is `included in wxPython >= 2.9.1 <included>`_. Until
\r
6 then, we'll avoid it because of the *nix build problems.
\r
8 This module hacks together a workaround to be used until 2.9.1 is
\r
9 widely installed (or at least released ;).
\r
12 http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
\r
20 def prop_from_argument(argument, curves=None, playlists=None):
\r
21 """Convert a :class:`~hooke.command.Argument` to a :class:`Property`.
\r
23 type = argument.type
\r
24 if type in ['driver']: # intentionally not handled (yet)
\r
26 if argument.count != 1:
\r
27 raise NotImplementedError(argument)
\r
29 'label':argument.name,
\r
30 'default':argument.default,
\r
31 'help':argument.help(),
\r
33 # type consolidation
\r
37 if type in ['string', 'bool', 'int', 'float', 'path']:
\r
38 _class = globals()['%sProperty' % type.capitalize()]
\r
39 return _class(**kwargs)
\r
40 elif type in ['curve', 'playlist']:
\r
42 choices = curves # extracted from the current playlist
\r
45 return ChoiceProperty(choices=choices, **kwargs)
\r
46 raise NotImplementedError(argument.type)
\r
48 def prop_from_setting(setting):
\r
49 """Convert a :class:`~hooke.config.Setting` to a :class:`Property`.
\r
51 raise NotImplementedError()
\r
54 class Property (object):
\r
55 def __init__(self, type, label, default, help=None):
\r
58 self.default = default
\r
61 def get_editor(self):
\r
62 """Return a suitable grid editor.
\r
64 raise NotImplementedError()
\r
66 def get_renderer(self):
\r
67 """Return a suitable grid renderer.
\r
69 Returns `None` if no special renderer is required.
\r
73 def string_for_value(self, value):
\r
74 """Return a string representation of `value` for loading the table.
\r
78 def value_for_string(self, string):
\r
79 """Return the value represented by `string`.
\r
84 class StringProperty (Property):
\r
85 def __init__(self, **kwargs):
\r
86 assert 'type' not in kwargs, kwargs
\r
87 if 'default' not in kwargs:
\r
88 kwargs['default'] = 0
\r
89 super(StringProperty, self).__init__(type='string', **kwargs)
\r
91 def get_editor(self):
\r
92 return wx.grid.GridCellTextEditor()
\r
94 def get_renderer(self):
\r
95 return wx.grid.GridCellStringRenderer()
\r
98 class BoolProperty (Property):
\r
99 """A boolean property.
\r
103 Unfortunately, changing a boolean property takes two clicks:
\r
105 1) create the editor
\r
106 2) change the value
\r
108 There are `ways around this`_, but it's not pretty.
\r
110 .. _ways around this:
\r
111 http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
\r
113 def __init__(self, **kwargs):
\r
114 assert 'type' not in kwargs, kwargs
\r
115 if 'default' not in kwargs:
\r
116 kwargs['default'] = True
\r
117 super(BoolProperty, self).__init__(type='bool', **kwargs)
\r
119 def get_editor(self):
\r
120 return wx.grid.GridCellBoolEditor()
\r
122 def get_renderer(self):
\r
123 return wx.grid.GridCellBoolRenderer()
\r
125 def string_for_value(self, value):
\r
130 def value_for_string(self, string):
\r
131 return string == '1'
\r
134 class IntProperty (Property):
\r
135 def __init__(self, **kwargs):
\r
136 assert 'type' not in kwargs, kwargs
\r
137 if 'default' not in kwargs:
\r
138 kwargs['default'] = 0
\r
139 super(IntProperty, self).__init__(type='int', **kwargs)
\r
141 def get_editor(self):
\r
142 return wx.grid.GridCellNumberEditor()
\r
144 def get_renderer(self):
\r
145 return wx.grid.GridCellNumberRenderer()
\r
147 def value_for_string(self, string):
\r
151 class FloatProperty (Property):
\r
152 def __init__(self, **kwargs):
\r
153 assert 'type' not in kwargs, kwargs
\r
154 if 'default' not in kwargs:
\r
155 kwargs['default'] = 0.0
\r
156 super(FloatProperty, self).__init__(type='float', **kwargs)
\r
158 def get_editor(self):
\r
159 return wx.grid.GridCellFloatEditor()
\r
161 def get_renderer(self):
\r
162 return wx.grid.GridCellFloatRenderer()
\r
164 def value_for_string(self, string):
\r
165 return float(string)
\r
168 class ChoiceProperty (Property):
\r
169 def __init__(self, choices, **kwargs):
\r
170 assert 'type' not in kwargs, kwargs
\r
171 if 'default' in kwargs:
\r
172 if kwargs['default'] not in choices:
\r
173 choices.insert(0, kwargs['default'])
\r
175 kwargs['default'] = choices[0]
\r
176 super(ChoiceProperty, self).__init__(type='choice', **kwargs)
\r
177 self._choices = choices
\r
179 def get_editor(self):
\r
180 choices = [self.string_for_value(c) for c in self._choices]
\r
181 return wx.grid.GridCellChoiceEditor(choices=choices)
\r
183 def get_renderer(self):
\r
185 #return wx.grid.GridCellChoiceRenderer()
\r
187 def string_for_value(self, value):
\r
188 if hasattr(value, 'name'):
\r
192 def value_for_string(self, string):
\r
193 for choice in self._choices:
\r
194 if self.string_for_value(choice) == string:
\r
196 raise ValueError(string)
\r
199 class PathProperty (StringProperty):
\r
200 """Simple file or path property.
\r
202 Currently there isn't a fancy file-picker popup. Perhaps in the
\r
205 def __init__(self, **kwargs):
\r
206 super(PathProperty, self).__init__(**kwargs)
\r
210 class PropertyPanel(Panel, wx.grid.Grid):
\r
211 """UI to view/set config values and command argsuments.
\r
213 def __init__(self, callbacks=None, **kwargs):
\r
214 super(PropertyPanel, self).__init__(
\r
215 name='property editor', callbacks=callbacks, **kwargs)
\r
216 self._properties = []
\r
218 self.CreateGrid(numRows=0, numCols=1)
\r
219 self.SetColLabelValue(0, 'value')
\r
221 self._last_tooltip = None
\r
222 self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
\r
224 def _on_motion(self, event):
\r
225 """Enable tooltips.
\r
227 x,y = self.CalcUnscrolledPosition(event.GetPosition())
\r
228 col,row = self.XYToCell(x, y)
\r
229 if col == -1 or row == -1:
\r
232 msg = self._properties[row].help or ''
\r
233 if msg != self._last_tooltip:
\r
234 self._last_tooltip = msg
\r
235 event.GetEventObject().SetToolTipString(msg)
\r
237 def append_property(self, property):
\r
238 if len([p for p in self._properties if p.label == property.label]) > 0:
\r
239 raise ValueError(property) # property.label collision
\r
240 self._properties.append(property)
\r
241 row = len(self._properties) - 1
\r
242 self.AppendRows(numRows=1)
\r
243 self.SetRowLabelValue(row, property.label)
\r
244 self.SetCellEditor(row=row, col=0, editor=property.get_editor())
\r
245 r = property.get_renderer()
\r
247 self.SetCellRenderer(row=row, col=0, renderer=r)
\r
248 self.set_property(property.label, property.default)
\r
250 def remove_property(self, label):
\r
251 row,property = self._property_by_label(label)
\r
252 self._properties.pop(row)
\r
253 self.DeleteRows(pos=row)
\r
256 while(len(self._properties) > 0):
\r
257 self.remove_property(self._properties[-1].label)
\r
259 def set_property(self, label, value):
\r
260 row,property = self._property_by_label(label)
\r
261 self.SetCellValue(row=row, col=0, s=property.string_for_value(value))
\r
263 def get_property(self, label):
\r
264 row,property = self._property_by_label(label)
\r
265 string = self.GetCellValue(row=row, col=0)
\r
266 return property.value_for_string(string)
\r
268 def get_values(self):
\r
269 return dict([(p.label, self.get_property(p.label))
\r
270 for p in self._properties])
\r
272 def _property_by_label(self, label):
\r
273 props = [(i,p) for i,p in enumerate(self._properties)
\r
274 if p.label == label]
\r
275 assert len(props) == 1, props
\r
276 row,property = props[0]
\r
277 return (row, property)
\r