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