Ran update_copyright.py
[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
36
37 def prop_from_argument(argument, curves=None, playlists=None):
38     """Convert a :class:`~hooke.command.Argument` to a :class:`Property`.
39     """
40     type = argument.type
41     if type in ['driver']:  # intentionally not handled (yet)
42         return None
43     if argument.count != 1:
44         raise NotImplementedError(argument)
45     kwargs = {
46         'label':argument.name,
47         'default':argument.default,
48         'help':argument.help(),
49         }
50     # type consolidation
51     if type == 'file':
52         type = 'path'
53     # type handling
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']:
58         if type == 'curve':
59             choices = curves  # extracted from the current playlist
60         else:
61             choices = playlists
62         return ChoiceProperty(choices=choices, **kwargs)
63     raise NotImplementedError(argument.type)
64
65 def prop_from_setting(setting):
66     """Convert a :class:`~hooke.config.Setting` to a :class:`Property`.
67     """    
68     raise NotImplementedError()
69
70
71 class Property (object):
72     def __init__(self, type, label, default, help=None):
73         self.type = type
74         self.label = label
75         self.default = default
76         self.help = help
77
78     def get_editor(self):
79         """Return a suitable grid editor.
80         """
81         raise NotImplementedError()
82
83     def get_renderer(self):
84         """Return a suitable grid renderer.
85
86         Returns `None` if no special renderer is required.
87         """
88         return None
89
90     def string_for_value(self, value):
91         """Return a string representation of `value` for loading the table.
92         """
93         return str(value)
94
95     def value_for_string(self, string):
96         """Return the value represented by `string`.
97         """
98         return string
99
100
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)
107
108     def get_editor(self):
109         return wx.grid.GridCellTextEditor()
110
111     def get_renderer(self):
112         return wx.grid.GridCellStringRenderer()
113
114
115 class BoolProperty (Property):
116     """A boolean property.
117
118     Notes
119     -----
120     Unfortunately, changing a boolean property takes two clicks:
121
122     1) create the editor
123     2) change the value
124
125     There are `ways around this`_, but it's not pretty.
126
127     .. _ways around this:
128       http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
129     """
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)
135
136     def get_editor(self):
137         return wx.grid.GridCellBoolEditor()
138
139     def get_renderer(self):
140         return wx.grid.GridCellBoolRenderer()
141
142     def string_for_value(self, value):
143         if value == True:
144             return '1'
145         return ''
146
147     def value_for_string(self, string):
148         return string == '1'
149
150
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)
157
158     def get_editor(self):
159         return wx.grid.GridCellNumberEditor()
160
161     def get_renderer(self):
162         return wx.grid.GridCellNumberRenderer()
163
164     def value_for_string(self, string):
165         return int(string)
166
167
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)
174
175     def get_editor(self):
176         return wx.grid.GridCellFloatEditor()
177
178     def get_renderer(self):
179         return wx.grid.GridCellFloatRenderer()
180
181     def value_for_string(self, string):
182         return float(string)
183
184
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'])
191         else:
192             kwargs['default'] = choices[0]
193         super(ChoiceProperty, self).__init__(type='choice', **kwargs)
194         self._choices = choices
195
196     def get_editor(self):
197         choices = [self.string_for_value(c) for c in self._choices]
198         return wx.grid.GridCellChoiceEditor(choices=choices)
199
200     def get_renderer(self):
201         return None
202         #return wx.grid.GridCellChoiceRenderer()
203
204     def string_for_value(self, value):
205         if hasattr(value, 'name'):
206             return value.name
207         return str(value)
208
209     def value_for_string(self, string):
210         for choice in self._choices:
211             if self.string_for_value(choice) == string:
212                return choice
213         raise ValueError(string)
214
215
216 class PathProperty (StringProperty):
217     """Simple file or path property.
218
219     Currently there isn't a fancy file-picker popup.  Perhaps in the
220     future.
221     """
222     def __init__(self, **kwargs):
223         super(PathProperty, self).__init__(**kwargs)
224         self.type = 'path'
225
226
227 class PropertyPanel(Panel, wx.grid.Grid):
228     """UI to view/set config values and command argsuments.
229     """
230     def __init__(self, callbacks=None, **kwargs):
231         super(PropertyPanel, self).__init__(
232             name='property editor', callbacks=callbacks, **kwargs)
233         self._properties = []
234
235         self.CreateGrid(numRows=0, numCols=1)
236         self.SetColLabelValue(0, 'value')
237
238         self._last_tooltip = None
239         self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
240
241     def _on_motion(self, event):
242         """Enable tooltips.
243         """
244         x,y = self.CalcUnscrolledPosition(event.GetPosition())
245         col,row = self.XYToCell(x, y)
246         if col == -1 or row == -1:
247             msg = ''
248         else:
249             msg = self._properties[row].help or ''
250         if msg != self._last_tooltip:
251             self._last_tooltip = msg
252             event.GetEventObject().SetToolTipString(msg)
253
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()
263         if r != None:
264             self.SetCellRenderer(row=row, col=0, renderer=r)
265         self.set_property(property.label, property.default)
266
267     def remove_property(self, label):
268         row,property = self._property_by_label(label)
269         self._properties.pop(row)
270         self.DeleteRows(pos=row)
271
272     def clear(self):
273         while(len(self._properties) > 0):
274             self.remove_property(self._properties[-1].label)
275
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))
279
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)
284
285     def get_values(self):
286         return dict([(p.label, self.get_property(p.label))
287                      for p in self._properties])
288
289     def _property_by_label(self, label):
290         props = [(i,p) for i,p in enumerate(self._properties)
291                  if p.label == label]
292         assert len(props) == 1, props
293         row,property = props[0]
294         return (row, property)