ab2518d3c34b8aaa3bbb57d87a5800999bbffe8e
[hooke.git] / hooke / ui / gui / panel / propertyeditor.py
1 # Copyright\r
2 \r
3 """Property editor panel for Hooke.\r
4 \r
5 wxPropertyGrid is `included in wxPython >= 2.9.1 <included>`_.  Until\r
6 then, we'll avoid it because of the *nix build problems.\r
7 \r
8 This module hacks together a workaround to be used until 2.9.1 is\r
9 widely installed (or at least released ;).\r
10 \r
11 .. _included:\r
12   http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download\r
13 """\r
14 \r
15 import wx.grid\r
16 \r
17 from . import Panel\r
18 \r
19 \r
20 def prop_from_argument(argument, curves=None, playlists=None):\r
21     """Convert a :class:`~hooke.command.Argument` to a :class:`Property`.\r
22     """\r
23     type = argument.type\r
24     if type in ['driver']:  # intentionally not handled (yet)\r
25         return None\r
26     if argument.count != 1:\r
27         raise NotImplementedError(argument)\r
28     kwargs = {\r
29         'label':argument.name,\r
30         'default':argument.default,\r
31         'help':argument.help(),\r
32         }\r
33     # type consolidation\r
34     if type == 'file':\r
35         type = 'path'\r
36     # type handling\r
37     if type in ['string', 'bool', 'int', 'float', 'path']:\r
38         _class = globals()['%sProperty' % type.capitalize()]\r
39         return _class(**kwargs)\r
40     elif type in ['curve', 'playlist']:\r
41         if type == 'curve':\r
42             choices = curves  # extracted from the current playlist\r
43         else:\r
44             choices = playlists\r
45         return ChoiceProperty(choices=choices, **kwargs)\r
46     raise NotImplementedError(argument.type)\r
47 \r
48 def prop_from_setting(setting):\r
49     """Convert a :class:`~hooke.config.Setting` to a :class:`Property`.\r
50     """    \r
51     raise NotImplementedError()\r
52 \r
53 \r
54 class Property (object):\r
55     def __init__(self, type, label, default, help=None):\r
56         self.type = type\r
57         self.label = label\r
58         self.default = default\r
59         self.help = help\r
60 \r
61     def get_editor(self):\r
62         """Return a suitable grid editor.\r
63         """\r
64         raise NotImplementedError()\r
65 \r
66     def get_renderer(self):\r
67         """Return a suitable grid renderer.\r
68 \r
69         Returns `None` if no special renderer is required.\r
70         """\r
71         return None\r
72 \r
73     def string_for_value(self, value):\r
74         """Return a string representation of `value` for loading the table.\r
75         """\r
76         return str(value)\r
77 \r
78     def value_for_string(self, string):\r
79         """Return the value represented by `string`.\r
80         """\r
81         return string\r
82 \r
83 \r
84 class StringProperty (Property):\r
85     def __init__(self, **kwargs):\r
86         assert 'type' not in kwargs, kwargs\r
87         if 'default' not in kwargs:\r
88             kwargs['default'] = 0\r
89         super(StringProperty, self).__init__(type='string', **kwargs)\r
90 \r
91     def get_editor(self):\r
92         return wx.grid.GridCellTextEditor()\r
93 \r
94     def get_renderer(self):\r
95         return wx.grid.GridCellStringRenderer()\r
96 \r
97 \r
98 class BoolProperty (Property):\r
99     """A boolean property.\r
100 \r
101     Notes\r
102     -----\r
103     Unfortunately, changing a boolean property takes two clicks:\r
104 \r
105     1) create the editor\r
106     2) change the value\r
107 \r
108     There are `ways around this`_, but it's not pretty.\r
109 \r
110     .. _ways around this:\r
111       http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click\r
112     """\r
113     def __init__(self, **kwargs):\r
114         assert 'type' not in kwargs, kwargs\r
115         if 'default' not in kwargs:\r
116             kwargs['default'] = True\r
117         super(BoolProperty, self).__init__(type='bool', **kwargs)\r
118 \r
119     def get_editor(self):\r
120         return wx.grid.GridCellBoolEditor()\r
121 \r
122     def get_renderer(self):\r
123         return wx.grid.GridCellBoolRenderer()\r
124 \r
125     def string_for_value(self, value):\r
126         if value == True:\r
127             return '1'\r
128         return ''\r
129 \r
130     def value_for_string(self, string):\r
131         return string == '1'\r
132 \r
133 \r
134 class IntProperty (Property):\r
135     def __init__(self, **kwargs):\r
136         assert 'type' not in kwargs, kwargs\r
137         if 'default' not in kwargs:\r
138             kwargs['default'] = 0\r
139         super(IntProperty, self).__init__(type='int', **kwargs)\r
140 \r
141     def get_editor(self):\r
142         return wx.grid.GridCellNumberEditor()\r
143 \r
144     def get_renderer(self):\r
145         return wx.grid.GridCellNumberRenderer()\r
146 \r
147     def value_for_string(self, string):\r
148         return int(string)\r
149 \r
150 \r
151 class FloatProperty (Property):\r
152     def __init__(self, **kwargs):\r
153         assert 'type' not in kwargs, kwargs\r
154         if 'default' not in kwargs:\r
155             kwargs['default'] = 0.0\r
156         super(FloatProperty, self).__init__(type='float', **kwargs)\r
157 \r
158     def get_editor(self):\r
159         return wx.grid.GridCellFloatEditor()\r
160 \r
161     def get_renderer(self):\r
162         return wx.grid.GridCellFloatRenderer()\r
163 \r
164     def value_for_string(self, string):\r
165         return float(string)\r
166 \r
167 \r
168 class ChoiceProperty (Property):\r
169     def __init__(self, choices, **kwargs):\r
170         assert 'type' not in kwargs, kwargs\r
171         if 'default' in kwargs:\r
172             if kwargs['default'] not in choices:\r
173                 choices.insert(0, kwargs['default'])\r
174         else:\r
175             kwargs['default'] = choices[0]\r
176         super(ChoiceProperty, self).__init__(type='choice', **kwargs)\r
177         self._choices = choices\r
178 \r
179     def get_editor(self):\r
180         choices = [self.string_for_value(c) for c in self._choices]\r
181         return wx.grid.GridCellChoiceEditor(choices=choices)\r
182 \r
183     def get_renderer(self):\r
184         return None\r
185         #return wx.grid.GridCellChoiceRenderer()\r
186 \r
187     def string_for_value(self, value):\r
188         if hasattr(value, 'name'):\r
189             return value.name\r
190         return str(value)\r
191 \r
192     def value_for_string(self, string):\r
193         for choice in self._choices:\r
194             if self.string_for_value(choice) == string:\r
195                return choice\r
196         raise ValueError(string)\r
197 \r
198 \r
199 class PathProperty (StringProperty):\r
200     """Simple file or path property.\r
201 \r
202     Currently there isn't a fancy file-picker popup.  Perhaps in the\r
203     future.\r
204     """\r
205     def __init__(self, **kwargs):\r
206         super(PathProperty, self).__init__(**kwargs)\r
207         self.type = 'path'\r
208 \r
209 \r
210 class PropertyPanel(Panel, wx.grid.Grid):\r
211     """UI to view/set config values and command argsuments.\r
212     """\r
213     def __init__(self, callbacks=None, **kwargs):\r
214         super(PropertyPanel, self).__init__(\r
215             name='property editor', callbacks=callbacks, **kwargs)\r
216         self._properties = []\r
217 \r
218         self.CreateGrid(numRows=0, numCols=1)\r
219         self.SetColLabelValue(0, 'value')\r
220 \r
221         self._last_tooltip = None\r
222         self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)\r
223 \r
224     def _on_motion(self, event):\r
225         """Enable tooltips.\r
226         """\r
227         x,y = self.CalcUnscrolledPosition(event.GetPosition())\r
228         col,row = self.XYToCell(x, y)\r
229         if col == -1 or row == -1:\r
230             msg = ''\r
231         else:\r
232             msg = self._properties[row].help or ''\r
233         if msg != self._last_tooltip:\r
234             self._last_tooltip = msg\r
235             event.GetEventObject().SetToolTipString(msg)\r
236 \r
237     def append_property(self, property):\r
238         if len([p for p in self._properties if p.label == property.label]) > 0:\r
239             raise ValueError(property)  # property.label collision\r
240         self._properties.append(property)\r
241         row = len(self._properties) - 1\r
242         self.AppendRows(numRows=1)\r
243         self.SetRowLabelValue(row, property.label)\r
244         self.SetCellEditor(row=row, col=0, editor=property.get_editor())\r
245         r = property.get_renderer()\r
246         if r != None:\r
247             self.SetCellRenderer(row=row, col=0, renderer=r)\r
248         self.set_property(property.label, property.default)\r
249 \r
250     def remove_property(self, label):\r
251         row,property = self._property_by_label(label)\r
252         self._properties.pop(row)\r
253         self.DeleteRows(pos=row)\r
254 \r
255     def clear(self):\r
256         while(len(self._properties) > 0):\r
257             self.remove_property(self._properties[-1].label)\r
258 \r
259     def set_property(self, label, value):\r
260         row,property = self._property_by_label(label)\r
261         self.SetCellValue(row=row, col=0, s=property.string_for_value(value))\r
262 \r
263     def get_property(self, label):\r
264         row,property = self._property_by_label(label)\r
265         string = self.GetCellValue(row=row, col=0)\r
266         return property.value_for_string(string)\r
267 \r
268     def get_values(self):\r
269         return dict([(p.label, self.get_property(p.label))\r
270                      for p in self._properties])\r
271 \r
272     def _property_by_label(self, label):\r
273         props = [(i,p) for i,p in enumerate(self._properties)\r
274                  if p.label == label]\r
275         assert len(props) == 1, props\r
276         row,property = props[0]\r
277         return (row, property)\r