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', 'dict']: # intentionally not handled (yet)
46 count = argument.count
48 count = 3 # HACK: should allow unlimited entries (somehow...)
49 argument._display_count = count # suport HACK in execute_command()
51 #'label':argument.name,
52 'default':argument.default,
53 'help':argument.help(),
55 type = ANALOGS.get(type, type) # type consolidation
57 if type in ['string', 'bool', 'int', 'float', 'path']:
58 _class = globals()['%sProperty' % type.capitalize()]
59 elif type in ['curve', 'playlist']:
61 choices = curves # extracted from the current playlist
65 _class = ChoiceProperty
66 kwargs['choices'] = choices
68 raise NotImplementedError(argument.type)
70 labels = [argument.name]
72 labels = ['%s %d' % (argument.name, i) for i in range(count)]
73 return [(label, _class(label=label, **kwargs)) for label in labels]
76 def props_from_setting(setting):
77 """Convert a :class:`~hooke.config.Setting` to a list of
80 # TODO: move props_from_argument code here and use
81 # argument_to_setting there.
82 raise NotImplementedError()
85 class Property (object):
86 def __init__(self, type, label, default, help=None):
89 self.default = default
93 """Return a suitable grid editor.
95 raise NotImplementedError()
97 def get_renderer(self):
98 """Return a suitable grid renderer.
100 Returns `None` if no special renderer is required.
104 def string_for_value(self, value):
105 """Return a string representation of `value` for loading the table.
107 return to_string(value, 'string')
109 def value_for_string(self, string):
110 """Return the value represented by `string`.
112 return from_string(string, 'string')
115 class StringProperty (Property):
116 def __init__(self, **kwargs):
117 assert 'type' not in kwargs, kwargs
118 if 'default' not in kwargs:
119 kwargs['default'] = 0
120 super(StringProperty, self).__init__(type='string', **kwargs)
122 def get_editor(self):
123 return wx.grid.GridCellTextEditor()
125 def get_renderer(self):
126 return wx.grid.GridCellStringRenderer()
129 class BoolProperty (Property):
130 """A boolean property.
134 Unfortunately, changing a boolean property takes two clicks:
139 There are `ways around this`_, but it's not pretty.
141 .. _ways around this:
142 http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
144 def __init__(self, **kwargs):
145 assert 'type' not in kwargs, kwargs
146 if 'default' not in kwargs:
147 kwargs['default'] = True
148 super(BoolProperty, self).__init__(type='bool', **kwargs)
150 def get_editor(self):
151 return wx.grid.GridCellBoolEditor()
153 def get_renderer(self):
154 return wx.grid.GridCellBoolRenderer()
156 def string_for_value(self, value):
161 def value_for_string(self, string):
165 class IntProperty (Property):
166 def __init__(self, **kwargs):
167 assert 'type' not in kwargs, kwargs
168 if 'default' not in kwargs:
169 kwargs['default'] = 0
170 super(IntProperty, self).__init__(type='int', **kwargs)
172 def get_editor(self):
173 return wx.grid.GridCellNumberEditor()
175 def get_renderer(self):
176 return wx.grid.GridCellNumberRenderer()
178 def value_for_string(self, string):
179 return from_string(string, 'int')
182 class FloatProperty (Property):
183 def __init__(self, **kwargs):
184 assert 'type' not in kwargs, kwargs
185 if 'default' not in kwargs:
186 kwargs['default'] = 0.0
187 super(FloatProperty, self).__init__(type='float', **kwargs)
189 def get_editor(self):
190 return wx.grid.GridCellFloatEditor()
192 def get_renderer(self):
193 return wx.grid.GridCellFloatRenderer()
195 def value_for_string(self, string):
196 return from_string(string, 'float')
199 class ChoiceProperty (Property):
200 def __init__(self, choices, **kwargs):
201 assert 'type' not in kwargs, kwargs
202 if 'default' in kwargs:
203 if kwargs['default'] not in choices:
204 choices.insert(0, kwargs['default'])
206 kwargs['default'] = choices[0]
207 super(ChoiceProperty, self).__init__(type='choice', **kwargs)
208 self._choices = choices
210 def get_editor(self):
211 choices = [self.string_for_value(c) for c in self._choices]
212 return wx.grid.GridCellChoiceEditor(choices=choices)
214 def get_renderer(self):
216 #return wx.grid.GridCellChoiceRenderer()
218 def string_for_value(self, value):
219 if hasattr(value, 'name'):
223 def value_for_string(self, string):
224 for choice in self._choices:
225 if self.string_for_value(choice) == string:
227 raise ValueError(string)
230 class PathProperty (StringProperty):
231 """Simple file or path property.
233 Currently there isn't a fancy file-picker popup. Perhaps in the
236 def __init__(self, **kwargs):
237 super(PathProperty, self).__init__(**kwargs)
241 class PropertyPanel(Panel, wx.grid.Grid):
242 """UI to view/set config values and command argsuments.
244 def __init__(self, callbacks=None, **kwargs):
245 super(PropertyPanel, self).__init__(
246 name='property editor', callbacks=callbacks, **kwargs)
247 self._properties = []
249 self.CreateGrid(numRows=0, numCols=1)
250 self.SetColLabelValue(0, 'value')
252 self._last_tooltip = None
253 self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
255 def _on_motion(self, event):
258 x,y = self.CalcUnscrolledPosition(event.GetPosition())
259 row,col = self.XYToCell(x, y)
260 if col == -1 or row == -1:
263 msg = self._properties[row].help or ''
264 if msg != self._last_tooltip:
265 self._last_tooltip = msg
266 event.GetEventObject().SetToolTipString(msg)
268 def append_property(self, property):
269 if len([p for p in self._properties if p.label == property.label]) > 0:
270 raise ValueError(property) # property.label collision
271 self._properties.append(property)
272 row = len(self._properties) - 1
273 self.AppendRows(numRows=1)
274 self.SetRowLabelValue(row, property.label)
275 self.SetCellEditor(row=row, col=0, editor=property.get_editor())
276 r = property.get_renderer()
278 self.SetCellRenderer(row=row, col=0, renderer=r)
279 self.set_property(property.label, property.default)
281 def remove_property(self, label):
282 row,property = self._property_by_label(label)
283 self._properties.pop(row)
284 self.DeleteRows(pos=row)
287 while(len(self._properties) > 0):
288 self.remove_property(self._properties[-1].label)
290 def set_property(self, label, value):
291 row,property = self._property_by_label(label)
292 self.SetCellValue(row=row, col=0, s=property.string_for_value(value))
294 def get_property(self, label):
295 row,property = self._property_by_label(label)
296 string = self.GetCellValue(row=row, col=0)
297 return property.value_for_string(string)
299 def get_values(self):
300 return dict([(p.label, self.get_property(p.label))
301 for p in self._properties])
303 def _property_by_label(self, label):
304 props = [(i,p) for i,p in enumerate(self._properties)
306 assert len(props) == 1, props
307 row,property = props[0]
308 return (row, property)