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
37 def prop_from_argument(argument, curves=None, playlists=None):
38 """Convert a :class:`~hooke.command.Argument` to a :class:`Property`.
41 if type in ['driver']: # intentionally not handled (yet)
43 if argument.count != 1:
44 raise NotImplementedError(argument)
46 'label':argument.name,
47 'default':argument.default,
48 'help':argument.help(),
54 if type in ['string', 'bool', 'int', 'float', 'path']:
55 _class = globals()['%sProperty' % type.capitalize()]
56 return _class(**kwargs)
57 elif type in ['curve', 'playlist']:
59 choices = curves # extracted from the current playlist
62 return ChoiceProperty(choices=choices, **kwargs)
63 raise NotImplementedError(argument.type)
65 def prop_from_setting(setting):
66 """Convert a :class:`~hooke.config.Setting` to a :class:`Property`.
68 raise NotImplementedError()
71 class Property (object):
72 def __init__(self, type, label, default, help=None):
75 self.default = default
79 """Return a suitable grid editor.
81 raise NotImplementedError()
83 def get_renderer(self):
84 """Return a suitable grid renderer.
86 Returns `None` if no special renderer is required.
90 def string_for_value(self, value):
91 """Return a string representation of `value` for loading the table.
95 def value_for_string(self, string):
96 """Return the value represented by `string`.
101 class StringProperty (Property):
102 def __init__(self, **kwargs):
103 assert 'type' not in kwargs, kwargs
104 if 'default' not in kwargs:
105 kwargs['default'] = 0
106 super(StringProperty, self).__init__(type='string', **kwargs)
108 def get_editor(self):
109 return wx.grid.GridCellTextEditor()
111 def get_renderer(self):
112 return wx.grid.GridCellStringRenderer()
115 class BoolProperty (Property):
116 """A boolean property.
120 Unfortunately, changing a boolean property takes two clicks:
125 There are `ways around this`_, but it's not pretty.
127 .. _ways around this:
128 http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
130 def __init__(self, **kwargs):
131 assert 'type' not in kwargs, kwargs
132 if 'default' not in kwargs:
133 kwargs['default'] = True
134 super(BoolProperty, self).__init__(type='bool', **kwargs)
136 def get_editor(self):
137 return wx.grid.GridCellBoolEditor()
139 def get_renderer(self):
140 return wx.grid.GridCellBoolRenderer()
142 def string_for_value(self, value):
147 def value_for_string(self, string):
151 class IntProperty (Property):
152 def __init__(self, **kwargs):
153 assert 'type' not in kwargs, kwargs
154 if 'default' not in kwargs:
155 kwargs['default'] = 0
156 super(IntProperty, self).__init__(type='int', **kwargs)
158 def get_editor(self):
159 return wx.grid.GridCellNumberEditor()
161 def get_renderer(self):
162 return wx.grid.GridCellNumberRenderer()
164 def value_for_string(self, string):
168 class FloatProperty (Property):
169 def __init__(self, **kwargs):
170 assert 'type' not in kwargs, kwargs
171 if 'default' not in kwargs:
172 kwargs['default'] = 0.0
173 super(FloatProperty, self).__init__(type='float', **kwargs)
175 def get_editor(self):
176 return wx.grid.GridCellFloatEditor()
178 def get_renderer(self):
179 return wx.grid.GridCellFloatRenderer()
181 def value_for_string(self, string):
185 class ChoiceProperty (Property):
186 def __init__(self, choices, **kwargs):
187 assert 'type' not in kwargs, kwargs
188 if 'default' in kwargs:
189 if kwargs['default'] not in choices:
190 choices.insert(0, kwargs['default'])
192 kwargs['default'] = choices[0]
193 super(ChoiceProperty, self).__init__(type='choice', **kwargs)
194 self._choices = choices
196 def get_editor(self):
197 choices = [self.string_for_value(c) for c in self._choices]
198 return wx.grid.GridCellChoiceEditor(choices=choices)
200 def get_renderer(self):
202 #return wx.grid.GridCellChoiceRenderer()
204 def string_for_value(self, value):
205 if hasattr(value, 'name'):
209 def value_for_string(self, string):
210 for choice in self._choices:
211 if self.string_for_value(choice) == string:
213 raise ValueError(string)
216 class PathProperty (StringProperty):
217 """Simple file or path property.
219 Currently there isn't a fancy file-picker popup. Perhaps in the
222 def __init__(self, **kwargs):
223 super(PathProperty, self).__init__(**kwargs)
227 class PropertyPanel(Panel, wx.grid.Grid):
228 """UI to view/set config values and command argsuments.
230 def __init__(self, callbacks=None, **kwargs):
231 super(PropertyPanel, self).__init__(
232 name='property editor', callbacks=callbacks, **kwargs)
233 self._properties = []
235 self.CreateGrid(numRows=0, numCols=1)
236 self.SetColLabelValue(0, 'value')
238 self._last_tooltip = None
239 self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
241 def _on_motion(self, event):
244 x,y = self.CalcUnscrolledPosition(event.GetPosition())
245 col,row = self.XYToCell(x, y)
246 if col == -1 or row == -1:
249 msg = self._properties[row].help or ''
250 if msg != self._last_tooltip:
251 self._last_tooltip = msg
252 event.GetEventObject().SetToolTipString(msg)
254 def append_property(self, property):
255 if len([p for p in self._properties if p.label == property.label]) > 0:
256 raise ValueError(property) # property.label collision
257 self._properties.append(property)
258 row = len(self._properties) - 1
259 self.AppendRows(numRows=1)
260 self.SetRowLabelValue(row, property.label)
261 self.SetCellEditor(row=row, col=0, editor=property.get_editor())
262 r = property.get_renderer()
264 self.SetCellRenderer(row=row, col=0, renderer=r)
265 self.set_property(property.label, property.default)
267 def remove_property(self, label):
268 row,property = self._property_by_label(label)
269 self._properties.pop(row)
270 self.DeleteRows(pos=row)
273 while(len(self._properties) > 0):
274 self.remove_property(self._properties[-1].label)
276 def set_property(self, label, value):
277 row,property = self._property_by_label(label)
278 self.SetCellValue(row=row, col=0, s=property.string_for_value(value))
280 def get_property(self, label):
281 row,property = self._property_by_label(label)
282 string = self.GetCellValue(row=row, col=0)
283 return property.value_for_string(string)
285 def get_values(self):
286 return dict([(p.label, self.get_property(p.label))
287 for p in self._properties])
289 def _property_by_label(self, label):
290 props = [(i,p) for i,p in enumerate(self._properties)
292 assert len(props) == 1, props
293 row,property = props[0]
294 return (row, property)