95233fcf8bf1daca8e94a0ad8d3931d4a9c24f8a
[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, to_string, from_string
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     count = argument.count
47     if count == -1:
48         count = 3  # HACK: should allow unlimited entries (somehow...)
49         argument._display_count = count  # suport HACK in execute_command()
50     kwargs = {
51         #'label':argument.name,
52         'default':argument.default,
53         'help':argument.help(),
54         }
55     type = ANALOGS.get(type, type)  # type consolidation
56     # type handling
57     if type in ['string', 'bool', 'int', 'float', 'path']:
58         _class = globals()['%sProperty' % type.capitalize()]
59     elif type in ['curve', 'playlist']:
60         if type == 'curve':
61             choices = curves  # extracted from the current playlist
62         else:
63             choices = playlists
64         properties = []
65         _class = ChoiceProperty
66         kwargs['choices'] = choices
67     else:
68         raise NotImplementedError(argument.type)
69     if count == 1:
70         labels = [argument.name]
71     else:
72         labels = ['%s %d' % (argument.name, i) for i in range(count)]
73     return [(label, _class(label=label, **kwargs)) for label in labels]
74    
75
76 def props_from_setting(setting):
77     """Convert a :class:`~hooke.config.Setting` to a list of
78     :class:`Property`\s.
79     """    
80     # TODO: move props_from_argument code here and use
81     # argument_to_setting there.
82     raise NotImplementedError()
83
84
85 class Property (object):
86     def __init__(self, type, label, default, help=None):
87         self.type = type
88         self.label = label
89         self.default = default
90         self.help = help
91
92     def get_editor(self):
93         """Return a suitable grid editor.
94         """
95         raise NotImplementedError()
96
97     def get_renderer(self):
98         """Return a suitable grid renderer.
99
100         Returns `None` if no special renderer is required.
101         """
102         return None
103
104     def string_for_value(self, value):
105         """Return a string representation of `value` for loading the table.
106         """
107         return to_string(value, 'string')
108
109     def value_for_string(self, string):
110         """Return the value represented by `string`.
111         """
112         return from_string(string, 'string')
113
114
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)
121
122     def get_editor(self):
123         return wx.grid.GridCellTextEditor()
124
125     def get_renderer(self):
126         return wx.grid.GridCellStringRenderer()
127
128
129 class BoolProperty (Property):
130     """A boolean property.
131
132     Notes
133     -----
134     Unfortunately, changing a boolean property takes two clicks:
135
136     1) create the editor
137     2) change the value
138
139     There are `ways around this`_, but it's not pretty.
140
141     .. _ways around this:
142       http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
143     """
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)
149
150     def get_editor(self):
151         return wx.grid.GridCellBoolEditor()
152
153     def get_renderer(self):
154         return wx.grid.GridCellBoolRenderer()
155
156     def string_for_value(self, value):
157         if value == True:
158             return '1'
159         return ''
160
161     def value_for_string(self, string):
162         return string == '1'
163
164
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)
171
172     def get_editor(self):
173         return wx.grid.GridCellNumberEditor()
174
175     def get_renderer(self):
176         return wx.grid.GridCellNumberRenderer()
177
178     def value_for_string(self, string):
179         return from_string(string, 'int')
180
181
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)
188
189     def get_editor(self):
190         return wx.grid.GridCellFloatEditor()
191
192     def get_renderer(self):
193         return wx.grid.GridCellFloatRenderer()
194
195     def value_for_string(self, string):
196         return from_string(string, 'float')
197
198
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'])
205         else:
206             kwargs['default'] = choices[0]
207         super(ChoiceProperty, self).__init__(type='choice', **kwargs)
208         self._choices = choices
209
210     def get_editor(self):
211         choices = [self.string_for_value(c) for c in self._choices]
212         return wx.grid.GridCellChoiceEditor(choices=choices)
213
214     def get_renderer(self):
215         return None
216         #return wx.grid.GridCellChoiceRenderer()
217
218     def string_for_value(self, value):
219         if hasattr(value, 'name'):
220             return value.name
221         return str(value)
222
223     def value_for_string(self, string):
224         for choice in self._choices:
225             if self.string_for_value(choice) == string:
226                return choice
227         raise ValueError(string)
228
229
230 class PathProperty (StringProperty):
231     """Simple file or path property.
232
233     Currently there isn't a fancy file-picker popup.  Perhaps in the
234     future.
235     """
236     def __init__(self, **kwargs):
237         super(PathProperty, self).__init__(**kwargs)
238         self.type = 'path'
239
240
241 class PropertyPanel(Panel, wx.grid.Grid):
242     """UI to view/set config values and command argsuments.
243     """
244     def __init__(self, callbacks=None, **kwargs):
245         super(PropertyPanel, self).__init__(
246             name='property editor', callbacks=callbacks, **kwargs)
247         self._properties = []
248
249         self.CreateGrid(numRows=0, numCols=1)
250         self.SetColLabelValue(0, 'value')
251
252         self._last_tooltip = None
253         self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
254
255     def _on_motion(self, event):
256         """Enable tooltips.
257         """
258         x,y = self.CalcUnscrolledPosition(event.GetPosition())
259         row,col = self.XYToCell(x, y)
260         if col == -1 or row == -1:
261             msg = ''
262         else:
263             msg = self._properties[row].help or ''
264         if msg != self._last_tooltip:
265             self._last_tooltip = msg
266             event.GetEventObject().SetToolTipString(msg)
267
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()
277         if r != None:
278             self.SetCellRenderer(row=row, col=0, renderer=r)
279         self.set_property(property.label, property.default)
280
281     def remove_property(self, label):
282         row,property = self._property_by_label(label)
283         self._properties.pop(row)
284         self.DeleteRows(pos=row)
285
286     def clear(self):
287         while(len(self._properties) > 0):
288             self.remove_property(self._properties[-1].label)
289
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))
293
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)
298
299     def get_values(self):
300         return dict([(p.label, self.get_property(p.label))
301                      for p in self._properties])
302
303     def _property_by_label(self, label):
304         props = [(i,p) for i,p in enumerate(self._properties)
305                  if p.label == label]
306         assert len(props) == 1, props
307         row,property = props[0]
308         return (row, property)