3bd571f24b443f3071f4e43e8f2201715ef8035b
[hooke.git] / hooke / ui / gui / panel / propertyeditor.py
1 # Copyright (C) 2010 Massimo Sandal <devicerandom@gmail.com>
2 #                    W. Trevor King <wking@drexel.edu>
3 #
4 # This file is part of Hooke.
5 #
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.
10 #
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.
15 #
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/>.
19
20 """Property editor panel for Hooke.
21
22 wxPropertyGrid is `included in wxPython >= 2.9.1 <included>`_.  Until
23 then, we'll avoid it because of the *nix build problems.
24
25 This module hacks together a workaround to be used until 2.9.1 is
26 widely installed (or at least released ;).
27
28 .. _included:
29   http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
30 """
31
32 import wx.grid
33
34 from . import Panel
35 from ....plugin import argument_to_setting
36 from ....util.convert import ANALOGS
37
38
39 def props_from_argument(argument, curves=None, playlists=None):
40     """Convert a :class:`~hooke.command.Argument` to a list of
41     :class:`Property`\s.
42     """
43     type = argument.type
44     if type in ['driver']:  # intentionally not handled (yet)
45         return None
46     if argument.count == -1:
47         raise NotImplementedError(argument)  # TODO: maybe just set count to 1?
48     kwargs = {
49         #'label':argument.name,
50         'default':argument.default,
51         'help':argument.help(),
52         }
53     type = ANALOGS.get(type, type)  # type consolidation
54     # type handling
55     if type in ['string', 'bool', 'int', 'float', 'path']:
56         _class = globals()['%sProperty' % type.capitalize()]
57     elif type in ['curve', 'playlist']:
58         if type == 'curve':
59             choices = curves  # extracted from the current playlist
60         else:
61             choices = playlists
62         properties = []
63         _class = ChoiceProperty
64         kwargs['choices'] = choices
65     else:
66         raise NotImplementedError(argument.type)
67     if argument.count == 1:
68         labels = [argument.name]
69     else:
70         labels = ['%s %d' % (argument.name, i) for i in range(argument.count)]
71     return [(label, _class(label=label, **kwargs)) for label in labels]
72    
73
74 def props_from_setting(setting):
75     """Convert a :class:`~hooke.config.Setting` to a list of
76     :class:`Property`\s.
77     """    
78     # TODO: move props_from_argument code here and use
79     # argument_to_setting there.
80     raise NotImplementedError()
81
82
83 class Property (object):
84     def __init__(self, type, label, default, help=None):
85         self.type = type
86         self.label = label
87         self.default = default
88         self.help = help
89
90     def get_editor(self):
91         """Return a suitable grid editor.
92         """
93         raise NotImplementedError()
94
95     def get_renderer(self):
96         """Return a suitable grid renderer.
97
98         Returns `None` if no special renderer is required.
99         """
100         return None
101
102     def string_for_value(self, value):
103         """Return a string representation of `value` for loading the table.
104         """
105         return str(value)
106
107     def value_for_string(self, string):
108         """Return the value represented by `string`.
109         """
110         return string
111
112
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)
119
120     def get_editor(self):
121         return wx.grid.GridCellTextEditor()
122
123     def get_renderer(self):
124         return wx.grid.GridCellStringRenderer()
125
126
127 class BoolProperty (Property):
128     """A boolean property.
129
130     Notes
131     -----
132     Unfortunately, changing a boolean property takes two clicks:
133
134     1) create the editor
135     2) change the value
136
137     There are `ways around this`_, but it's not pretty.
138
139     .. _ways around this:
140       http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
141     """
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)
147
148     def get_editor(self):
149         return wx.grid.GridCellBoolEditor()
150
151     def get_renderer(self):
152         return wx.grid.GridCellBoolRenderer()
153
154     def string_for_value(self, value):
155         if value == True:
156             return '1'
157         return ''
158
159     def value_for_string(self, string):
160         return string == '1'
161
162
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)
169
170     def get_editor(self):
171         return wx.grid.GridCellNumberEditor()
172
173     def get_renderer(self):
174         return wx.grid.GridCellNumberRenderer()
175
176     def value_for_string(self, string):
177         return int(string)
178
179
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)
186
187     def get_editor(self):
188         return wx.grid.GridCellFloatEditor()
189
190     def get_renderer(self):
191         return wx.grid.GridCellFloatRenderer()
192
193     def value_for_string(self, string):
194         return float(string)
195
196
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'])
203         else:
204             kwargs['default'] = choices[0]
205         super(ChoiceProperty, self).__init__(type='choice', **kwargs)
206         self._choices = choices
207
208     def get_editor(self):
209         choices = [self.string_for_value(c) for c in self._choices]
210         return wx.grid.GridCellChoiceEditor(choices=choices)
211
212     def get_renderer(self):
213         return None
214         #return wx.grid.GridCellChoiceRenderer()
215
216     def string_for_value(self, value):
217         if hasattr(value, 'name'):
218             return value.name
219         return str(value)
220
221     def value_for_string(self, string):
222         for choice in self._choices:
223             if self.string_for_value(choice) == string:
224                return choice
225         raise ValueError(string)
226
227
228 class PathProperty (StringProperty):
229     """Simple file or path property.
230
231     Currently there isn't a fancy file-picker popup.  Perhaps in the
232     future.
233     """
234     def __init__(self, **kwargs):
235         super(PathProperty, self).__init__(**kwargs)
236         self.type = 'path'
237
238
239 class PropertyPanel(Panel, wx.grid.Grid):
240     """UI to view/set config values and command argsuments.
241     """
242     def __init__(self, callbacks=None, **kwargs):
243         super(PropertyPanel, self).__init__(
244             name='property editor', callbacks=callbacks, **kwargs)
245         self._properties = []
246
247         self.CreateGrid(numRows=0, numCols=1)
248         self.SetColLabelValue(0, 'value')
249
250         self._last_tooltip = None
251         self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
252
253     def _on_motion(self, event):
254         """Enable tooltips.
255         """
256         x,y = self.CalcUnscrolledPosition(event.GetPosition())
257         row,col = self.XYToCell(x, y)
258         if col == -1 or row == -1:
259             msg = ''
260         else:
261             msg = self._properties[row].help or ''
262         if msg != self._last_tooltip:
263             self._last_tooltip = msg
264             event.GetEventObject().SetToolTipString(msg)
265
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()
275         if r != None:
276             self.SetCellRenderer(row=row, col=0, renderer=r)
277         self.set_property(property.label, property.default)
278
279     def remove_property(self, label):
280         row,property = self._property_by_label(label)
281         self._properties.pop(row)
282         self.DeleteRows(pos=row)
283
284     def clear(self):
285         while(len(self._properties) > 0):
286             self.remove_property(self._properties[-1].label)
287
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))
291
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)
296
297     def get_values(self):
298         return dict([(p.label, self.get_property(p.label))
299                      for p in self._properties])
300
301     def _property_by_label(self, label):
302         props = [(i,p) for i,p in enumerate(self._properties)
303                  if p.label == label]
304         assert len(props) == 1, props
305         row,property = props[0]
306         return (row, property)