Add support to GUI's property editor for argument.count > 1.
[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     labels = ['%s %d' % (argument.name, i) for i in range(argument.count)]
68     return [(label, _class(label=label, **kwargs)) for label in labels]
69    
70
71 def props_from_setting(setting):
72     """Convert a :class:`~hooke.config.Setting` to a list of
73     :class:`Property`\s.
74     """    
75     # TODO: move props_from_argument code here and use
76     # argument_to_setting there.
77     raise NotImplementedError()
78
79
80 class Property (object):
81     def __init__(self, type, label, default, help=None):
82         self.type = type
83         self.label = label
84         self.default = default
85         self.help = help
86
87     def get_editor(self):
88         """Return a suitable grid editor.
89         """
90         raise NotImplementedError()
91
92     def get_renderer(self):
93         """Return a suitable grid renderer.
94
95         Returns `None` if no special renderer is required.
96         """
97         return None
98
99     def string_for_value(self, value):
100         """Return a string representation of `value` for loading the table.
101         """
102         return str(value)
103
104     def value_for_string(self, string):
105         """Return the value represented by `string`.
106         """
107         return string
108
109
110 class StringProperty (Property):
111     def __init__(self, **kwargs):
112         assert 'type' not in kwargs, kwargs
113         if 'default' not in kwargs:
114             kwargs['default'] = 0
115         super(StringProperty, self).__init__(type='string', **kwargs)
116
117     def get_editor(self):
118         return wx.grid.GridCellTextEditor()
119
120     def get_renderer(self):
121         return wx.grid.GridCellStringRenderer()
122
123
124 class BoolProperty (Property):
125     """A boolean property.
126
127     Notes
128     -----
129     Unfortunately, changing a boolean property takes two clicks:
130
131     1) create the editor
132     2) change the value
133
134     There are `ways around this`_, but it's not pretty.
135
136     .. _ways around this:
137       http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
138     """
139     def __init__(self, **kwargs):
140         assert 'type' not in kwargs, kwargs
141         if 'default' not in kwargs:
142             kwargs['default'] = True
143         super(BoolProperty, self).__init__(type='bool', **kwargs)
144
145     def get_editor(self):
146         return wx.grid.GridCellBoolEditor()
147
148     def get_renderer(self):
149         return wx.grid.GridCellBoolRenderer()
150
151     def string_for_value(self, value):
152         if value == True:
153             return '1'
154         return ''
155
156     def value_for_string(self, string):
157         return string == '1'
158
159
160 class IntProperty (Property):
161     def __init__(self, **kwargs):
162         assert 'type' not in kwargs, kwargs
163         if 'default' not in kwargs:
164             kwargs['default'] = 0
165         super(IntProperty, self).__init__(type='int', **kwargs)
166
167     def get_editor(self):
168         return wx.grid.GridCellNumberEditor()
169
170     def get_renderer(self):
171         return wx.grid.GridCellNumberRenderer()
172
173     def value_for_string(self, string):
174         return int(string)
175
176
177 class FloatProperty (Property):
178     def __init__(self, **kwargs):
179         assert 'type' not in kwargs, kwargs
180         if 'default' not in kwargs:
181             kwargs['default'] = 0.0
182         super(FloatProperty, self).__init__(type='float', **kwargs)
183
184     def get_editor(self):
185         return wx.grid.GridCellFloatEditor()
186
187     def get_renderer(self):
188         return wx.grid.GridCellFloatRenderer()
189
190     def value_for_string(self, string):
191         return float(string)
192
193
194 class ChoiceProperty (Property):
195     def __init__(self, choices, **kwargs):
196         assert 'type' not in kwargs, kwargs
197         if 'default' in kwargs:
198             if kwargs['default'] not in choices:
199                 choices.insert(0, kwargs['default'])
200         else:
201             kwargs['default'] = choices[0]
202         super(ChoiceProperty, self).__init__(type='choice', **kwargs)
203         self._choices = choices
204
205     def get_editor(self):
206         choices = [self.string_for_value(c) for c in self._choices]
207         return wx.grid.GridCellChoiceEditor(choices=choices)
208
209     def get_renderer(self):
210         return None
211         #return wx.grid.GridCellChoiceRenderer()
212
213     def string_for_value(self, value):
214         if hasattr(value, 'name'):
215             return value.name
216         return str(value)
217
218     def value_for_string(self, string):
219         for choice in self._choices:
220             if self.string_for_value(choice) == string:
221                return choice
222         raise ValueError(string)
223
224
225 class PathProperty (StringProperty):
226     """Simple file or path property.
227
228     Currently there isn't a fancy file-picker popup.  Perhaps in the
229     future.
230     """
231     def __init__(self, **kwargs):
232         super(PathProperty, self).__init__(**kwargs)
233         self.type = 'path'
234
235
236 class PropertyPanel(Panel, wx.grid.Grid):
237     """UI to view/set config values and command argsuments.
238     """
239     def __init__(self, callbacks=None, **kwargs):
240         super(PropertyPanel, self).__init__(
241             name='property editor', callbacks=callbacks, **kwargs)
242         self._properties = []
243
244         self.CreateGrid(numRows=0, numCols=1)
245         self.SetColLabelValue(0, 'value')
246
247         self._last_tooltip = None
248         self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
249
250     def _on_motion(self, event):
251         """Enable tooltips.
252         """
253         x,y = self.CalcUnscrolledPosition(event.GetPosition())
254         row,col = self.XYToCell(x, y)
255         if col == -1 or row == -1:
256             msg = ''
257         else:
258             msg = self._properties[row].help or ''
259         if msg != self._last_tooltip:
260             self._last_tooltip = msg
261             event.GetEventObject().SetToolTipString(msg)
262
263     def append_property(self, property):
264         if len([p for p in self._properties if p.label == property.label]) > 0:
265             raise ValueError(property)  # property.label collision
266         self._properties.append(property)
267         row = len(self._properties) - 1
268         self.AppendRows(numRows=1)
269         self.SetRowLabelValue(row, property.label)
270         self.SetCellEditor(row=row, col=0, editor=property.get_editor())
271         r = property.get_renderer()
272         if r != None:
273             self.SetCellRenderer(row=row, col=0, renderer=r)
274         self.set_property(property.label, property.default)
275
276     def remove_property(self, label):
277         row,property = self._property_by_label(label)
278         self._properties.pop(row)
279         self.DeleteRows(pos=row)
280
281     def clear(self):
282         while(len(self._properties) > 0):
283             self.remove_property(self._properties[-1].label)
284
285     def set_property(self, label, value):
286         row,property = self._property_by_label(label)
287         self.SetCellValue(row=row, col=0, s=property.string_for_value(value))
288
289     def get_property(self, label):
290         row,property = self._property_by_label(label)
291         string = self.GetCellValue(row=row, col=0)
292         return property.value_for_string(string)
293
294     def get_values(self):
295         return dict([(p.label, self.get_property(p.label))
296                      for p in self._properties])
297
298     def _property_by_label(self, label):
299         props = [(i,p) for i,p in enumerate(self._properties)
300                  if p.label == label]
301         assert len(props) == 1, props
302         row,property = props[0]
303         return (row, property)