1 # Copyright (C) 2010 Massimo Sandal <devicerandom@gmail.com>
2 # W. Trevor King <wking@drexel.edu>
4 # This file is part of Hooke.
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.
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.
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/>.
20 """Property editor panel for Hooke.
22 wxPropertyGrid is `included in wxPython >= 2.9.1 <included>`_. Until
23 then, we'll avoid it because of the *nix build problems.
25 This module hacks together a workaround to be used until 2.9.1 is
26 widely installed (or at least released ;).
29 http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
35 from ....plugin import argument_to_setting
36 from ....util.convert import ANALOGS, to_string, from_string
39 def props_from_argument(argument, curves=None, playlists=None):
40 """Convert a :class:`~hooke.command.Argument` to a list of
44 if type in ['driver']: # intentionally not handled (yet)
46 if argument.count == -1:
47 raise NotImplementedError(argument) # TODO: maybe just set count to 1?
49 #'label':argument.name,
50 'default':argument.default,
51 'help':argument.help(),
53 type = ANALOGS.get(type, type) # type consolidation
55 if type in ['string', 'bool', 'int', 'float', 'path']:
56 _class = globals()['%sProperty' % type.capitalize()]
57 elif type in ['curve', 'playlist']:
59 choices = curves # extracted from the current playlist
63 _class = ChoiceProperty
64 kwargs['choices'] = choices
66 raise NotImplementedError(argument.type)
67 if argument.count == 1:
68 labels = [argument.name]
70 labels = ['%s %d' % (argument.name, i) for i in range(argument.count)]
71 return [(label, _class(label=label, **kwargs)) for label in labels]
74 def props_from_setting(setting):
75 """Convert a :class:`~hooke.config.Setting` to a list of
78 # TODO: move props_from_argument code here and use
79 # argument_to_setting there.
80 raise NotImplementedError()
83 class Property (object):
84 def __init__(self, type, label, default, help=None):
87 self.default = default
91 """Return a suitable grid editor.
93 raise NotImplementedError()
95 def get_renderer(self):
96 """Return a suitable grid renderer.
98 Returns `None` if no special renderer is required.
102 def string_for_value(self, value):
103 """Return a string representation of `value` for loading the table.
105 return to_string(value, 'string')
107 def value_for_string(self, string):
108 """Return the value represented by `string`.
110 return from_string(string, 'string')
113 class StringProperty (Property):
114 def __init__(self, **kwargs):
115 assert 'type' not in kwargs, kwargs
116 if 'default' not in kwargs:
117 kwargs['default'] = 0
118 super(StringProperty, self).__init__(type='string', **kwargs)
120 def get_editor(self):
121 return wx.grid.GridCellTextEditor()
123 def get_renderer(self):
124 return wx.grid.GridCellStringRenderer()
127 class BoolProperty (Property):
128 """A boolean property.
132 Unfortunately, changing a boolean property takes two clicks:
137 There are `ways around this`_, but it's not pretty.
139 .. _ways around this:
140 http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
142 def __init__(self, **kwargs):
143 assert 'type' not in kwargs, kwargs
144 if 'default' not in kwargs:
145 kwargs['default'] = True
146 super(BoolProperty, self).__init__(type='bool', **kwargs)
148 def get_editor(self):
149 return wx.grid.GridCellBoolEditor()
151 def get_renderer(self):
152 return wx.grid.GridCellBoolRenderer()
154 def string_for_value(self, value):
159 def value_for_string(self, string):
163 class IntProperty (Property):
164 def __init__(self, **kwargs):
165 assert 'type' not in kwargs, kwargs
166 if 'default' not in kwargs:
167 kwargs['default'] = 0
168 super(IntProperty, self).__init__(type='int', **kwargs)
170 def get_editor(self):
171 return wx.grid.GridCellNumberEditor()
173 def get_renderer(self):
174 return wx.grid.GridCellNumberRenderer()
176 def value_for_string(self, string):
177 return from_string(string, 'int')
180 class FloatProperty (Property):
181 def __init__(self, **kwargs):
182 assert 'type' not in kwargs, kwargs
183 if 'default' not in kwargs:
184 kwargs['default'] = 0.0
185 super(FloatProperty, self).__init__(type='float', **kwargs)
187 def get_editor(self):
188 return wx.grid.GridCellFloatEditor()
190 def get_renderer(self):
191 return wx.grid.GridCellFloatRenderer()
193 def value_for_string(self, string):
194 return from_string(string, 'float')
197 class ChoiceProperty (Property):
198 def __init__(self, choices, **kwargs):
199 assert 'type' not in kwargs, kwargs
200 if 'default' in kwargs:
201 if kwargs['default'] not in choices:
202 choices.insert(0, kwargs['default'])
204 kwargs['default'] = choices[0]
205 super(ChoiceProperty, self).__init__(type='choice', **kwargs)
206 self._choices = choices
208 def get_editor(self):
209 choices = [self.string_for_value(c) for c in self._choices]
210 return wx.grid.GridCellChoiceEditor(choices=choices)
212 def get_renderer(self):
214 #return wx.grid.GridCellChoiceRenderer()
216 def string_for_value(self, value):
217 if hasattr(value, 'name'):
221 def value_for_string(self, string):
222 for choice in self._choices:
223 if self.string_for_value(choice) == string:
225 raise ValueError(string)
228 class PathProperty (StringProperty):
229 """Simple file or path property.
231 Currently there isn't a fancy file-picker popup. Perhaps in the
234 def __init__(self, **kwargs):
235 super(PathProperty, self).__init__(**kwargs)
239 class PropertyPanel(Panel, wx.grid.Grid):
240 """UI to view/set config values and command argsuments.
242 def __init__(self, callbacks=None, **kwargs):
243 super(PropertyPanel, self).__init__(
244 name='property editor', callbacks=callbacks, **kwargs)
245 self._properties = []
247 self.CreateGrid(numRows=0, numCols=1)
248 self.SetColLabelValue(0, 'value')
250 self._last_tooltip = None
251 self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
253 def _on_motion(self, event):
256 x,y = self.CalcUnscrolledPosition(event.GetPosition())
257 row,col = self.XYToCell(x, y)
258 if col == -1 or row == -1:
261 msg = self._properties[row].help or ''
262 if msg != self._last_tooltip:
263 self._last_tooltip = msg
264 event.GetEventObject().SetToolTipString(msg)
266 def append_property(self, property):
267 if len([p for p in self._properties if p.label == property.label]) > 0:
268 raise ValueError(property) # property.label collision
269 self._properties.append(property)
270 row = len(self._properties) - 1
271 self.AppendRows(numRows=1)
272 self.SetRowLabelValue(row, property.label)
273 self.SetCellEditor(row=row, col=0, editor=property.get_editor())
274 r = property.get_renderer()
276 self.SetCellRenderer(row=row, col=0, renderer=r)
277 self.set_property(property.label, property.default)
279 def remove_property(self, label):
280 row,property = self._property_by_label(label)
281 self._properties.pop(row)
282 self.DeleteRows(pos=row)
285 while(len(self._properties) > 0):
286 self.remove_property(self._properties[-1].label)
288 def set_property(self, label, value):
289 row,property = self._property_by_label(label)
290 self.SetCellValue(row=row, col=0, s=property.string_for_value(value))
292 def get_property(self, label):
293 row,property = self._property_by_label(label)
294 string = self.GetCellValue(row=row, col=0)
295 return property.value_for_string(string)
297 def get_values(self):
298 return dict([(p.label, self.get_property(p.label))
299 for p in self._properties])
301 def _property_by_label(self, label):
302 props = [(i,p) for i,p in enumerate(self._properties)
304 assert len(props) == 1, props
305 row,property = props[0]
306 return (row, property)