Better None-handling in convert and propertyeditor
[hooke.git] / hooke / ui / gui / panel / propertyeditor.py
index ab2518d3c34b8aaa3bbb57d87a5800999bbffe8e..5cc44690c3c8868d74c2eba549e1c274c1143625 100644 (file)
-# Copyright\r
-\r
-"""Property editor panel for Hooke.\r
-\r
-wxPropertyGrid is `included in wxPython >= 2.9.1 <included>`_.  Until\r
-then, we'll avoid it because of the *nix build problems.\r
-\r
-This module hacks together a workaround to be used until 2.9.1 is\r
-widely installed (or at least released ;).\r
-\r
-.. _included:\r
-  http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download\r
-"""\r
-\r
-import wx.grid\r
-\r
-from . import Panel\r
-\r
-\r
-def prop_from_argument(argument, curves=None, playlists=None):\r
-    """Convert a :class:`~hooke.command.Argument` to a :class:`Property`.\r
-    """\r
-    type = argument.type\r
-    if type in ['driver']:  # intentionally not handled (yet)\r
-        return None\r
-    if argument.count != 1:\r
-        raise NotImplementedError(argument)\r
-    kwargs = {\r
-        'label':argument.name,\r
-        'default':argument.default,\r
-        'help':argument.help(),\r
-        }\r
-    # type consolidation\r
-    if type == 'file':\r
-        type = 'path'\r
-    # type handling\r
-    if type in ['string', 'bool', 'int', 'float', 'path']:\r
-        _class = globals()['%sProperty' % type.capitalize()]\r
-        return _class(**kwargs)\r
-    elif type in ['curve', 'playlist']:\r
-        if type == 'curve':\r
-            choices = curves  # extracted from the current playlist\r
-        else:\r
-            choices = playlists\r
-        return ChoiceProperty(choices=choices, **kwargs)\r
-    raise NotImplementedError(argument.type)\r
-\r
-def prop_from_setting(setting):\r
-    """Convert a :class:`~hooke.config.Setting` to a :class:`Property`.\r
-    """    \r
-    raise NotImplementedError()\r
-\r
-\r
-class Property (object):\r
-    def __init__(self, type, label, default, help=None):\r
-        self.type = type\r
-        self.label = label\r
-        self.default = default\r
-        self.help = help\r
-\r
-    def get_editor(self):\r
-        """Return a suitable grid editor.\r
-        """\r
-        raise NotImplementedError()\r
-\r
-    def get_renderer(self):\r
-        """Return a suitable grid renderer.\r
-\r
-        Returns `None` if no special renderer is required.\r
-        """\r
-        return None\r
-\r
-    def string_for_value(self, value):\r
-        """Return a string representation of `value` for loading the table.\r
-        """\r
-        return str(value)\r
-\r
-    def value_for_string(self, string):\r
-        """Return the value represented by `string`.\r
-        """\r
-        return string\r
-\r
-\r
-class StringProperty (Property):\r
-    def __init__(self, **kwargs):\r
-        assert 'type' not in kwargs, kwargs\r
-        if 'default' not in kwargs:\r
-            kwargs['default'] = 0\r
-        super(StringProperty, self).__init__(type='string', **kwargs)\r
-\r
-    def get_editor(self):\r
-        return wx.grid.GridCellTextEditor()\r
-\r
-    def get_renderer(self):\r
-        return wx.grid.GridCellStringRenderer()\r
-\r
-\r
-class BoolProperty (Property):\r
-    """A boolean property.\r
-\r
-    Notes\r
-    -----\r
-    Unfortunately, changing a boolean property takes two clicks:\r
-\r
-    1) create the editor\r
-    2) change the value\r
-\r
-    There are `ways around this`_, but it's not pretty.\r
-\r
-    .. _ways around this:\r
-      http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click\r
-    """\r
-    def __init__(self, **kwargs):\r
-        assert 'type' not in kwargs, kwargs\r
-        if 'default' not in kwargs:\r
-            kwargs['default'] = True\r
-        super(BoolProperty, self).__init__(type='bool', **kwargs)\r
-\r
-    def get_editor(self):\r
-        return wx.grid.GridCellBoolEditor()\r
-\r
-    def get_renderer(self):\r
-        return wx.grid.GridCellBoolRenderer()\r
-\r
-    def string_for_value(self, value):\r
-        if value == True:\r
-            return '1'\r
-        return ''\r
-\r
-    def value_for_string(self, string):\r
-        return string == '1'\r
-\r
-\r
-class IntProperty (Property):\r
-    def __init__(self, **kwargs):\r
-        assert 'type' not in kwargs, kwargs\r
-        if 'default' not in kwargs:\r
-            kwargs['default'] = 0\r
-        super(IntProperty, self).__init__(type='int', **kwargs)\r
-\r
-    def get_editor(self):\r
-        return wx.grid.GridCellNumberEditor()\r
-\r
-    def get_renderer(self):\r
-        return wx.grid.GridCellNumberRenderer()\r
-\r
-    def value_for_string(self, string):\r
-        return int(string)\r
-\r
-\r
-class FloatProperty (Property):\r
-    def __init__(self, **kwargs):\r
-        assert 'type' not in kwargs, kwargs\r
-        if 'default' not in kwargs:\r
-            kwargs['default'] = 0.0\r
-        super(FloatProperty, self).__init__(type='float', **kwargs)\r
-\r
-    def get_editor(self):\r
-        return wx.grid.GridCellFloatEditor()\r
-\r
-    def get_renderer(self):\r
-        return wx.grid.GridCellFloatRenderer()\r
-\r
-    def value_for_string(self, string):\r
-        return float(string)\r
-\r
-\r
-class ChoiceProperty (Property):\r
-    def __init__(self, choices, **kwargs):\r
-        assert 'type' not in kwargs, kwargs\r
-        if 'default' in kwargs:\r
-            if kwargs['default'] not in choices:\r
-                choices.insert(0, kwargs['default'])\r
-        else:\r
-            kwargs['default'] = choices[0]\r
-        super(ChoiceProperty, self).__init__(type='choice', **kwargs)\r
-        self._choices = choices\r
-\r
-    def get_editor(self):\r
-        choices = [self.string_for_value(c) for c in self._choices]\r
-        return wx.grid.GridCellChoiceEditor(choices=choices)\r
-\r
-    def get_renderer(self):\r
-        return None\r
-        #return wx.grid.GridCellChoiceRenderer()\r
-\r
-    def string_for_value(self, value):\r
-        if hasattr(value, 'name'):\r
-            return value.name\r
-        return str(value)\r
-\r
-    def value_for_string(self, string):\r
-        for choice in self._choices:\r
-            if self.string_for_value(choice) == string:\r
-               return choice\r
-        raise ValueError(string)\r
-\r
-\r
-class PathProperty (StringProperty):\r
-    """Simple file or path property.\r
-\r
-    Currently there isn't a fancy file-picker popup.  Perhaps in the\r
-    future.\r
-    """\r
-    def __init__(self, **kwargs):\r
-        super(PathProperty, self).__init__(**kwargs)\r
-        self.type = 'path'\r
-\r
-\r
-class PropertyPanel(Panel, wx.grid.Grid):\r
-    """UI to view/set config values and command argsuments.\r
-    """\r
-    def __init__(self, callbacks=None, **kwargs):\r
-        super(PropertyPanel, self).__init__(\r
-            name='property editor', callbacks=callbacks, **kwargs)\r
-        self._properties = []\r
-\r
-        self.CreateGrid(numRows=0, numCols=1)\r
-        self.SetColLabelValue(0, 'value')\r
-\r
-        self._last_tooltip = None\r
-        self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)\r
-\r
-    def _on_motion(self, event):\r
-        """Enable tooltips.\r
-        """\r
-        x,y = self.CalcUnscrolledPosition(event.GetPosition())\r
-        col,row = self.XYToCell(x, y)\r
-        if col == -1 or row == -1:\r
-            msg = ''\r
-        else:\r
-            msg = self._properties[row].help or ''\r
-        if msg != self._last_tooltip:\r
-            self._last_tooltip = msg\r
-            event.GetEventObject().SetToolTipString(msg)\r
-\r
-    def append_property(self, property):\r
-        if len([p for p in self._properties if p.label == property.label]) > 0:\r
-            raise ValueError(property)  # property.label collision\r
-        self._properties.append(property)\r
-        row = len(self._properties) - 1\r
-        self.AppendRows(numRows=1)\r
-        self.SetRowLabelValue(row, property.label)\r
-        self.SetCellEditor(row=row, col=0, editor=property.get_editor())\r
-        r = property.get_renderer()\r
-        if r != None:\r
-            self.SetCellRenderer(row=row, col=0, renderer=r)\r
-        self.set_property(property.label, property.default)\r
-\r
-    def remove_property(self, label):\r
-        row,property = self._property_by_label(label)\r
-        self._properties.pop(row)\r
-        self.DeleteRows(pos=row)\r
-\r
-    def clear(self):\r
-        while(len(self._properties) > 0):\r
-            self.remove_property(self._properties[-1].label)\r
-\r
-    def set_property(self, label, value):\r
-        row,property = self._property_by_label(label)\r
-        self.SetCellValue(row=row, col=0, s=property.string_for_value(value))\r
-\r
-    def get_property(self, label):\r
-        row,property = self._property_by_label(label)\r
-        string = self.GetCellValue(row=row, col=0)\r
-        return property.value_for_string(string)\r
-\r
-    def get_values(self):\r
-        return dict([(p.label, self.get_property(p.label))\r
-                     for p in self._properties])\r
-\r
-    def _property_by_label(self, label):\r
-        props = [(i,p) for i,p in enumerate(self._properties)\r
-                 if p.label == label]\r
-        assert len(props) == 1, props\r
-        row,property = props[0]\r
-        return (row, property)\r
+# Copyright (C) 2010 Massimo Sandal <devicerandom@gmail.com>
+#                    W. Trevor King <wking@drexel.edu>
+#
+# This file is part of Hooke.
+#
+# Hooke is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Hooke is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
+# Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with Hooke.  If not, see
+# <http://www.gnu.org/licenses/>.
+
+"""Property editor panel for Hooke.
+
+wxPropertyGrid is `included in wxPython >= 2.9.1 <included>`_.  Until
+then, we'll avoid it because of the *nix build problems.
+
+This module hacks together a workaround to be used until 2.9.1 is
+widely installed (or at least released ;).
+
+.. _included:
+  http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
+"""
+
+import wx.grid
+
+from . import Panel
+from ....plugin import argument_to_setting
+from ....util.convert import ANALOGS, to_string, from_string
+
+
+def props_from_argument(argument, curves=None, playlists=None):
+    """Convert a :class:`~hooke.command.Argument` to a list of
+    :class:`Property`\s.
+    """
+    type = argument.type
+    if type in ['driver']:  # intentionally not handled (yet)
+        return None
+    if argument.count == -1:
+        raise NotImplementedError(argument)  # TODO: maybe just set count to 1?
+    kwargs = {
+        #'label':argument.name,
+        'default':argument.default,
+        'help':argument.help(),
+        }
+    type = ANALOGS.get(type, type)  # type consolidation
+    # type handling
+    if type in ['string', 'bool', 'int', 'float', 'path']:
+        _class = globals()['%sProperty' % type.capitalize()]
+    elif type in ['curve', 'playlist']:
+        if type == 'curve':
+            choices = curves  # extracted from the current playlist
+        else:
+            choices = playlists
+        properties = []
+        _class = ChoiceProperty
+        kwargs['choices'] = choices
+    else:
+        raise NotImplementedError(argument.type)
+    if argument.count == 1:
+        labels = [argument.name]
+    else:
+        labels = ['%s %d' % (argument.name, i) for i in range(argument.count)]
+    return [(label, _class(label=label, **kwargs)) for label in labels]
+   
+
+def props_from_setting(setting):
+    """Convert a :class:`~hooke.config.Setting` to a list of
+    :class:`Property`\s.
+    """    
+    # TODO: move props_from_argument code here and use
+    # argument_to_setting there.
+    raise NotImplementedError()
+
+
+class Property (object):
+    def __init__(self, type, label, default, help=None):
+        self.type = type
+        self.label = label
+        self.default = default
+        self.help = help
+
+    def get_editor(self):
+        """Return a suitable grid editor.
+        """
+        raise NotImplementedError()
+
+    def get_renderer(self):
+        """Return a suitable grid renderer.
+
+        Returns `None` if no special renderer is required.
+        """
+        return None
+
+    def string_for_value(self, value):
+        """Return a string representation of `value` for loading the table.
+        """
+        return to_string(value, 'string')
+
+    def value_for_string(self, string):
+        """Return the value represented by `string`.
+        """
+        return from_string(string, 'string')
+
+
+class StringProperty (Property):
+    def __init__(self, **kwargs):
+        assert 'type' not in kwargs, kwargs
+        if 'default' not in kwargs:
+            kwargs['default'] = 0
+        super(StringProperty, self).__init__(type='string', **kwargs)
+
+    def get_editor(self):
+        return wx.grid.GridCellTextEditor()
+
+    def get_renderer(self):
+        return wx.grid.GridCellStringRenderer()
+
+
+class BoolProperty (Property):
+    """A boolean property.
+
+    Notes
+    -----
+    Unfortunately, changing a boolean property takes two clicks:
+
+    1) create the editor
+    2) change the value
+
+    There are `ways around this`_, but it's not pretty.
+
+    .. _ways around this:
+      http://wiki.wxpython.org/Change%20wxGrid%20CheckBox%20with%20one%20click
+    """
+    def __init__(self, **kwargs):
+        assert 'type' not in kwargs, kwargs
+        if 'default' not in kwargs:
+            kwargs['default'] = True
+        super(BoolProperty, self).__init__(type='bool', **kwargs)
+
+    def get_editor(self):
+        return wx.grid.GridCellBoolEditor()
+
+    def get_renderer(self):
+        return wx.grid.GridCellBoolRenderer()
+
+    def string_for_value(self, value):
+        if value == True:
+            return '1'
+        return ''
+
+    def value_for_string(self, string):
+        return string == '1'
+
+
+class IntProperty (Property):
+    def __init__(self, **kwargs):
+        assert 'type' not in kwargs, kwargs
+        if 'default' not in kwargs:
+            kwargs['default'] = 0
+        super(IntProperty, self).__init__(type='int', **kwargs)
+
+    def get_editor(self):
+        return wx.grid.GridCellNumberEditor()
+
+    def get_renderer(self):
+        return wx.grid.GridCellNumberRenderer()
+
+    def value_for_string(self, string):
+        return from_string(string, 'int')
+
+
+class FloatProperty (Property):
+    def __init__(self, **kwargs):
+        assert 'type' not in kwargs, kwargs
+        if 'default' not in kwargs:
+            kwargs['default'] = 0.0
+        super(FloatProperty, self).__init__(type='float', **kwargs)
+
+    def get_editor(self):
+        return wx.grid.GridCellFloatEditor()
+
+    def get_renderer(self):
+        return wx.grid.GridCellFloatRenderer()
+
+    def value_for_string(self, string):
+        return from_string(string, 'float')
+
+
+class ChoiceProperty (Property):
+    def __init__(self, choices, **kwargs):
+        assert 'type' not in kwargs, kwargs
+        if 'default' in kwargs:
+            if kwargs['default'] not in choices:
+                choices.insert(0, kwargs['default'])
+        else:
+            kwargs['default'] = choices[0]
+        super(ChoiceProperty, self).__init__(type='choice', **kwargs)
+        self._choices = choices
+
+    def get_editor(self):
+        choices = [self.string_for_value(c) for c in self._choices]
+        return wx.grid.GridCellChoiceEditor(choices=choices)
+
+    def get_renderer(self):
+        return None
+        #return wx.grid.GridCellChoiceRenderer()
+
+    def string_for_value(self, value):
+        if hasattr(value, 'name'):
+            return value.name
+        return str(value)
+
+    def value_for_string(self, string):
+        for choice in self._choices:
+            if self.string_for_value(choice) == string:
+               return choice
+        raise ValueError(string)
+
+
+class PathProperty (StringProperty):
+    """Simple file or path property.
+
+    Currently there isn't a fancy file-picker popup.  Perhaps in the
+    future.
+    """
+    def __init__(self, **kwargs):
+        super(PathProperty, self).__init__(**kwargs)
+        self.type = 'path'
+
+
+class PropertyPanel(Panel, wx.grid.Grid):
+    """UI to view/set config values and command argsuments.
+    """
+    def __init__(self, callbacks=None, **kwargs):
+        super(PropertyPanel, self).__init__(
+            name='property editor', callbacks=callbacks, **kwargs)
+        self._properties = []
+
+        self.CreateGrid(numRows=0, numCols=1)
+        self.SetColLabelValue(0, 'value')
+
+        self._last_tooltip = None
+        self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_motion)
+
+    def _on_motion(self, event):
+        """Enable tooltips.
+        """
+        x,y = self.CalcUnscrolledPosition(event.GetPosition())
+        row,col = self.XYToCell(x, y)
+        if col == -1 or row == -1:
+            msg = ''
+        else:
+            msg = self._properties[row].help or ''
+        if msg != self._last_tooltip:
+            self._last_tooltip = msg
+            event.GetEventObject().SetToolTipString(msg)
+
+    def append_property(self, property):
+        if len([p for p in self._properties if p.label == property.label]) > 0:
+            raise ValueError(property)  # property.label collision
+        self._properties.append(property)
+        row = len(self._properties) - 1
+        self.AppendRows(numRows=1)
+        self.SetRowLabelValue(row, property.label)
+        self.SetCellEditor(row=row, col=0, editor=property.get_editor())
+        r = property.get_renderer()
+        if r != None:
+            self.SetCellRenderer(row=row, col=0, renderer=r)
+        self.set_property(property.label, property.default)
+
+    def remove_property(self, label):
+        row,property = self._property_by_label(label)
+        self._properties.pop(row)
+        self.DeleteRows(pos=row)
+
+    def clear(self):
+        while(len(self._properties) > 0):
+            self.remove_property(self._properties[-1].label)
+
+    def set_property(self, label, value):
+        row,property = self._property_by_label(label)
+        self.SetCellValue(row=row, col=0, s=property.string_for_value(value))
+
+    def get_property(self, label):
+        row,property = self._property_by_label(label)
+        string = self.GetCellValue(row=row, col=0)
+        return property.value_for_string(string)
+
+    def get_values(self):
+        return dict([(p.label, self.get_property(p.label))
+                     for p in self._properties])
+
+    def _property_by_label(self, label):
+        props = [(i,p) for i,p in enumerate(self._properties)
+                 if p.label == label]
+        assert len(props) == 1, props
+        row,property = props[0]
+        return (row, property)