1 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
13 # Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke. If not, see
17 # <http://www.gnu.org/licenses/>.
19 """Property editor panel for Hooke.
21 wxPropertyGrid is `included in wxPython >= 2.9.1 <included>`_. Until
22 then, we'll avoid it because of the *nix build problems.
24 This module hacks together a workaround to be used until 2.9.1 is
25 widely installed (or at least released ;).
28 http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
34 from ....plugin import argument_to_setting
35 from ....util.convert import ANALOGS, to_string, from_string
38 def props_from_argument(argument, curves=None, playlists=None):
39 """Convert a :class:`~hooke.command.Argument` to a list of
43 if type in ['driver', 'dict', 'command stack']:
44 return None # intentionally not handled (yet)
45 count = argument.count
47 count = 3 # HACK: should allow unlimited entries (somehow...)
48 argument._display_count = count # suport HACK in execute_command()
50 #'label':argument.name,
51 'default':argument.default,
52 'help':argument.help(),
54 type = ANALOGS.get(type, type) # type consolidation
56 if type in ['string', 'bool', 'int', 'float', 'path']:
57 _class = globals()['%sProperty' % type.capitalize()]
58 elif type in ['curve', 'playlist']:
60 choices = curves # extracted from the current playlist
64 _class = ChoiceProperty
65 kwargs['choices'] = choices
67 raise NotImplementedError(argument.type)
69 labels = [argument.name]
71 labels = ['%s %d' % (argument.name, i) for i in range(count)]
72 return [(label, _class(label=label, **kwargs)) for label in labels]
75 def props_from_setting(setting):
76 """Convert a :class:`~hooke.config.Setting` to a list of
79 # TODO: move props_from_argument code here and use
80 # argument_to_setting there.
81 raise NotImplementedError()
84 class Property (object):
85 def __init__(self, type, label, default, help=None):
88 self.default = default
92 """Return a suitable grid editor.
94 raise NotImplementedError()
96 def get_renderer(self):
97 """Return a suitable grid renderer.
99 Returns `None` if no special renderer is required.
103 def string_for_value(self, value):
104 """Return a string representation of `value` for loading the table.
106 return to_string(value, 'string')
108 def value_for_string(self, string):
109 """Return the value represented by `string`.
111 return from_string(string, 'string')
114 class StringProperty (Property):
115 def __init__(self, **kwargs):
116 assert 'type' not in kwargs, kwargs
117 if 'default' not in kwargs:
118 kwargs['default'] = 0
119 super(StringProperty, self).__init__(type='string', **kwargs)
121 def get_editor(self):
122 return wx.grid.GridCellTextEditor()
124 def get_renderer(self):
125 return wx.grid.GridCellStringRenderer()
128 class BoolProperty (Property):
129 """A boolean property.
133 Unfortunately, changing a boolean property takes two clicks:
138 There are `ways around this`_, but it's not pretty.
140 .. _ways around this:
141 http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
143 def __init__(self, **kwargs):
144 assert 'type' not in kwargs, kwargs
145 if 'default' not in kwargs:
146 kwargs['default'] = True
147 super(BoolProperty, self).__init__(type='bool', **kwargs)
149 def get_editor(self):
150 return wx.grid.GridCellBoolEditor()
152 def get_renderer(self):
153 return wx.grid.GridCellBoolRenderer()
155 def string_for_value(self, value):
160 def value_for_string(self, string):
164 class IntProperty (Property):
165 def __init__(self, **kwargs):
166 assert 'type' not in kwargs, kwargs
167 if 'default' not in kwargs:
168 kwargs['default'] = 0
169 super(IntProperty, self).__init__(type='int', **kwargs)
171 def get_editor(self):
172 return wx.grid.GridCellNumberEditor()
174 def get_renderer(self):
175 return wx.grid.GridCellNumberRenderer()
177 def value_for_string(self, string):
178 return from_string(string, 'int')
181 class FloatProperty (Property):
182 def __init__(self, **kwargs):
183 assert 'type' not in kwargs, kwargs
184 if 'default' not in kwargs:
185 kwargs['default'] = 0.0
186 super(FloatProperty, self).__init__(type='float', **kwargs)
188 def get_editor(self):
189 return wx.grid.GridCellFloatEditor()
191 def get_renderer(self):
192 return wx.grid.GridCellFloatRenderer()
194 def value_for_string(self, string):
195 return from_string(string, 'float')
198 class ChoiceProperty (Property):
199 def __init__(self, choices, **kwargs):
200 assert 'type' not in kwargs, kwargs
201 if 'default' in kwargs:
202 if kwargs['default'] not in choices:
203 choices.insert(0, kwargs['default'])
205 kwargs['default'] = choices[0]
206 super(ChoiceProperty, self).__init__(type='choice', **kwargs)
207 self._choices = choices
209 def get_editor(self):
210 choices = [self.string_for_value(c) for c in self._choices]
211 return wx.grid.GridCellChoiceEditor(choices=choices)
213 def get_renderer(self):
215 #return wx.grid.GridCellChoiceRenderer()
217 def string_for_value(self, value):
218 if hasattr(value, 'name'):
222 def value_for_string(self, string):
223 for choice in self._choices:
224 if self.string_for_value(choice) == string:
226 raise ValueError(string)
229 class PathProperty (StringProperty):
230 """Simple file or path property.
232 Currently there isn't a fancy file-picker popup. Perhaps in the
235 def __init__(self, **kwargs):
236 super(PathProperty, self).__init__(**kwargs)
240 class PropertyPanel(Panel, wx.grid.Grid):
241 """UI to view/set config values and command argsuments.
243 def __init__(self, callbacks=None, **kwargs):
244 super(PropertyPanel, self).__init__(
245 name='property editor', callbacks=callbacks, **kwargs)
246 self._properties = []
248 self.CreateGrid(numRows=0, numCols=1)
249 self.SetColLabelValue(0, 'value')
251 self._last_tooltip = None
252 self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
254 def _on_motion(self, event):
257 x,y = self.CalcUnscrolledPosition(event.GetPosition())
258 row,col = self.XYToCell(x, y)
259 if col == -1 or row == -1:
262 msg = self._properties[row].help or ''
263 if msg != self._last_tooltip:
264 self._last_tooltip = msg
265 event.GetEventObject().SetToolTipString(msg)
267 def append_property(self, property):
268 if len([p for p in self._properties if p.label == property.label]) > 0:
269 raise ValueError(property) # property.label collision
270 self._properties.append(property)
271 row = len(self._properties) - 1
272 self.AppendRows(numRows=1)
273 self.SetRowLabelValue(row, property.label)
274 self.SetCellEditor(row=row, col=0, editor=property.get_editor())
275 r = property.get_renderer()
277 self.SetCellRenderer(row=row, col=0, renderer=r)
278 self.set_property(property.label, property.default)
280 def remove_property(self, label):
281 row,property = self._property_by_label(label)
282 self._properties.pop(row)
283 self.DeleteRows(pos=row)
286 while(len(self._properties) > 0):
287 self.remove_property(self._properties[-1].label)
289 def set_property(self, label, value):
290 row,property = self._property_by_label(label)
291 self.SetCellValue(row=row, col=0, s=property.string_for_value(value))
293 def get_property(self, label):
294 row,property = self._property_by_label(label)
295 string = self.GetCellValue(row=row, col=0)
296 return property.value_for_string(string)
298 def get_values(self):
299 return dict([(p.label, self.get_property(p.label))
300 for p in self._properties])
302 def _property_by_label(self, label):
303 props = [(i,p) for i,p in enumerate(self._properties)
305 assert len(props) == 1, props
306 row,property = props[0]
307 return (row, property)