Several changes while getting 'plot' panel working
authorW. Trevor King <wking@drexel.edu>
Sat, 31 Jul 2010 17:27:54 +0000 (13:27 -0400)
committerW. Trevor King <wking@drexel.edu>
Sat, 31 Jul 2010 17:27:54 +0000 (13:27 -0400)
hooke/ui/gui/__init__.py
hooke/ui/gui/menu.py
hooke/ui/gui/panel/__init__.py
hooke/ui/gui/panel/playlist.py
hooke/ui/gui/panel/plot.py
hooke/ui/gui/panel/propertyeditor-propgrid.py [new file with mode: 0644]
hooke/ui/gui/panel/propertyeditor.py
hooke/ui/gui/panel/propertyeditor2.py [deleted file]

index b1e854d2995ee7d1317c6a67b6fbe62b7550696d..5dac90f936c84f579b42255b017cbdd16b415eda 100644 (file)
@@ -1,6 +1,7 @@
 # Copyright\r
 \r
 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.\r
+\r
 """\r
 \r
 WX_GOOD=['2.8']\r
@@ -18,15 +19,11 @@ import time
 import wx.html\r
 import wx.aui as aui\r
 import wx.lib.evtmgr as evtmgr\r
-\r
-\r
 # wxPropertyGrid is included in wxPython >= 2.9.1, see\r
 #   http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download\r
 # until then, we'll avoid it because of the *nix build problems.\r
 #import wx.propgrid as wxpg\r
 \r
-from matplotlib.ticker import FuncFormatter\r
-\r
 from ...command import CommandExit, Exit, Success, Failure, Command, Argument\r
 from ...config import Setting\r
 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig\r
@@ -36,7 +33,7 @@ from .dialog.save_file import select_save_file
 from . import menu as menu\r
 from . import navbar as navbar\r
 from . import panel as panel\r
-from .panel.propertyeditor2 import prop_from_argument, prop_from_setting\r
+from .panel.propertyeditor import prop_from_argument, prop_from_setting\r
 from . import prettyformat as prettyformat\r
 from . import statusbar as statusbar\r
 \r
@@ -68,7 +65,7 @@ class HookeFrame (wx.Frame):
         # Min size for the frame itself isn't completely done.  See\r
         # the end of FrameManager::Update() for the test code. For\r
         # now, just hard code a frame minimum size.\r
-        self.SetMinSize(wx.Size(500, 500))\r
+        #self.SetMinSize(wx.Size(500, 500))\r
 \r
         self._setup_panels()\r
         self._setup_toolbars()\r
@@ -76,8 +73,10 @@ class HookeFrame (wx.Frame):
 \r
         # Create the menubar after the panes so that the default\r
         # perspective is created with all panes open\r
+        panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]\r
         self._c['menu bar'] = menu.HookeMenuBar(\r
             parent=self,\r
+            panels=panels,\r
             callbacks={\r
                 'close': self._on_close,\r
                 'about': self._on_about,\r
@@ -96,19 +95,22 @@ class HookeFrame (wx.Frame):
         self._setup_perspectives()\r
         self._bind_events()\r
 \r
-        name = self.gui.config['active perspective']\r
+        self.execute_command(\r
+                command=self._command_by_name('load playlist'),\r
+                args={'input':'test/data/test'},\r
+                )\r
         return # TODO: cleanup\r
-        self.playlists = self._c['playlists'].Playlists\r
+        self.playlists = self._c['playlist'].Playlists\r
         self._displayed_plot = None\r
         #load default list, if possible\r
-        self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))\r
+        self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))\r
 \r
 \r
     # GUI maintenance\r
 \r
     def _setup_panels(self):\r
         client_size = self.GetClientSize()\r
-        for label,p,style in [\r
+        for p,style in [\r
 #            ('folders', wx.GenericDirCtrl(\r
 #                    parent=self,\r
 #                    dir=self.gui.config['folders-workdir'],\r
@@ -116,7 +118,7 @@ class HookeFrame (wx.Frame):
 #                    style=wx.DIRCTRL_SHOW_FILTERS,\r
 #                    filter=self.gui.config['folders-filters'],\r
 #                    defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'),  #HACK: config should convert\r
-            ('playlists', panel.PANELS['playlist'](\r
+            (panel.PANELS['playlist'](\r
                     callbacks={\r
                         'delete_playlist':self._on_user_delete_playlist,\r
                         '_delete_playlist':self._on_delete_playlist,\r
@@ -125,7 +127,6 @@ class HookeFrame (wx.Frame):
                         '_on_set_selected_playlist':self._on_set_selected_playlist,\r
                         '_on_set_selected_curve':self._on_set_selected_curve,\r
                         },\r
-                    config=self.gui.config,\r
                     parent=self,\r
                     style=wx.WANTS_CHARS|wx.NO_BORDER,\r
                     # WANTS_CHARS so the panel doesn't eat the Return key.\r
@@ -141,7 +142,7 @@ class HookeFrame (wx.Frame):
 #                    size=wx.Size(430, 200),\r
 #                    style=aui.AUI_NB_DEFAULT_STYLE\r
 #                    | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),\r
-            ('commands', panel.PANELS['commands'](\r
+            (panel.PANELS['commands'](\r
                     commands=self.commands,\r
                     selected=self.gui.config['selected command'],\r
                     callbacks={\r
@@ -155,7 +156,7 @@ class HookeFrame (wx.Frame):
                     # WANTS_CHARS so the panel doesn't eat the Return key.\r
 #                    size=(160, 200),\r
                     ), 'right'),\r
-            ('property', panel.PANELS['propertyeditor2'](\r
+            (panel.PANELS['propertyeditor'](\r
                     callbacks={},\r
                     parent=self,\r
                     style=wx.WANTS_CHARS,\r
@@ -166,7 +167,15 @@ class HookeFrame (wx.Frame):
 #                    pos=wx.Point(0, 0),\r
 #                    size=wx.Size(150, 90),\r
 #                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),\r
-            ('output', panel.PANELS['output'](\r
+            (panel.PANELS['plot'](\r
+                    callbacks={\r
+                        },\r
+                    parent=self,\r
+                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
+                    # WANTS_CHARS so the panel doesn't eat the Return key.\r
+#                    size=(160, 200),\r
+                    ), 'center'),\r
+            (panel.PANELS['output'](\r
                     parent=self,\r
                     pos=wx.Point(0, 0),\r
                     size=wx.Size(150, 90),\r
@@ -174,13 +183,13 @@ class HookeFrame (wx.Frame):
              'bottom'),\r
 #            ('results', panel.results.Results(self), 'bottom'),\r
             ]:\r
-            self._add_panel(label, p, style)\r
+            self._add_panel(p, style)\r
         #self._c['assistant'].SetEditable(False)\r
 \r
-    def _add_panel(self, label, panel, style):\r
-        self._c[label] = panel\r
-        cap_label = label.capitalize()\r
-        info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)\r
+    def _add_panel(self, panel, style):\r
+        self._c[panel.name] = panel\r
+        m_name = panel.managed_name\r
+        info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)\r
         info.PaneBorder(False).CloseButton(True).MaximizeButton(False)\r
         if style == 'top':\r
             info.Top()\r
@@ -263,6 +272,17 @@ class HookeFrame (wx.Frame):
 \r
 \r
 \r
+    # Panel utility functions\r
+\r
+    def _file_name(self, name):\r
+        """Cleanup names according to configured preferences.\r
+        """\r
+        if self.gui.config['hide extensions'] == 'True':  # HACK: config should decode\r
+            name,ext = os.path.splitext(name)\r
+        return name\r
+\r
+\r
+\r
     # Command handling\r
 \r
     def _command_by_name(self, name):\r
@@ -277,10 +297,10 @@ class HookeFrame (wx.Frame):
                         command=None, args=None):\r
         if args == None:\r
             args = {}\r
-        if ('property' in self._c\r
+        if ('property editor' in self._c\r
             and self.gui.config['selected command'] == command):\r
             arg_names = [arg.name for arg in command.arguments]\r
-            for name,value in self._c['property'].get_values().items():\r
+            for name,value in self._c['property editor'].get_values().items():\r
                 if name in arg_names:\r
                     args[name] = value\r
         print 'executing', command.name, args\r
@@ -353,22 +373,25 @@ class HookeFrame (wx.Frame):
         """\r
         if not isinstance(results[-1], Success):\r
             self._postprocess_text(command, results=results)\r
+            return\r
         assert len(results) == 2, results\r
         playlist = results[0]\r
-        self._c['playlists']._c['tree'].add_playlist(playlist)\r
+        self._c['playlist']._c['tree'].add_playlist(playlist)\r
 \r
     def _postprocess_get_playlist(self, command, args={}, results=[]):\r
         if not isinstance(results[-1], Success):\r
             self._postprocess_text(command, results=results)\r
+            return\r
         assert len(results) == 2, results\r
         playlist = results[0]\r
-        self._c['playlists']._c['tree'].update_playlist(playlist)\r
+        self._c['playlist']._c['tree'].update_playlist(playlist)\r
 \r
     def _postprocess_get_curve(self, command, args={}, results=[]):\r
         """Update `self` to show the curve.\r
         """\r
         if not isinstance(results[-1], Success):\r
             self._postprocess_text(command, results=results)\r
+            return\r
         assert len(results) == 2, results\r
         curve = results[0]\r
         if args.get('curve', None) == None:\r
@@ -378,8 +401,11 @@ class HookeFrame (wx.Frame):
             playlist = results[0]\r
         else:\r
             raise NotImplementedError()\r
-        self._c['playlists']._c['tree'].set_selected_curve(\r
-            playlist, curve)\r
+        if 'playlist' in self._c:\r
+            self._c['playlist']._c['tree'].set_selected_curve(\r
+                playlist, curve)\r
+        if 'plot' in self._c:\r
+            self._c['plot'].set_curve(curve, config=self.gui.config)\r
 \r
     def _postprocess_next_curve(self, command, args={}, results=[]):\r
         """No-op.  Only call 'next curve' via `self._next_curve()`.\r
@@ -397,16 +423,16 @@ class HookeFrame (wx.Frame):
     def _GetActiveFileIndex(self):\r
         lib.playlist.Playlist = self.GetActivePlaylist()\r
         #get the selected item from the tree\r
-        selected_item = self._c['playlists']._c['tree'].GetSelection()\r
+        selected_item = self._c['playlist']._c['tree'].GetSelection()\r
         #test if a playlist or a curve was double-clicked\r
-        if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
+        if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):\r
             return -1\r
         else:\r
             count = 0\r
-            selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
+            selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)\r
             while selected_item.IsOk():\r
                 count += 1\r
-                selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
+                selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)\r
             return count\r
 \r
     def _GetPlaylistTab(self, name):\r
@@ -568,92 +594,6 @@ class HookeFrame (wx.Frame):
                 self.UpdateNote()\r
                 self.UpdatePlot()\r
 \r
-    def UpdatePlot(self, plot=None):\r
-\r
-        def add_to_plot(curve, set_scale=True):\r
-            if curve.visible and curve.x and curve.y:\r
-                #get the index of the subplot to use as destination\r
-                destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1\r
-                #set all parameters for the plot\r
-                axes_list[destination].set_title(curve.title)\r
-                if set_scale:\r
-                    axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)\r
-                    axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)\r
-                    #set the formatting details for the scale\r
-                    formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)\r
-                    formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)\r
-                    axes_list[destination].xaxis.set_major_formatter(formatter_x)\r
-                    axes_list[destination].yaxis.set_major_formatter(formatter_y)\r
-                if curve.style == 'plot':\r
-                    axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)\r
-                if curve.style == 'scatter':\r
-                    axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)\r
-                #add the legend if necessary\r
-                if curve.legend:\r
-                    axes_list[destination].legend()\r
-\r
-        if plot is None:\r
-            active_file = self.GetActiveFile()\r
-            if not active_file.driver:\r
-                #the first time we identify a file, the following need to be set\r
-                active_file.identify(self.drivers)\r
-                for curve in active_file.plot.curves:\r
-                    curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')\r
-                    curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')\r
-                    curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')\r
-                    curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')\r
-                    curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')\r
-            if active_file.driver is None:\r
-                self.AppendToOutput('Invalid file: ' + active_file.filename)\r
-                return\r
-            self.displayed_plot = copy.deepcopy(active_file.plot)\r
-            #add raw curves to plot\r
-            self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)\r
-            #apply all active plotmanipulators\r
-            self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)\r
-            #add corrected curves to plot\r
-            self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)\r
-        else:\r
-            active_file = None\r
-            self.displayed_plot = copy.deepcopy(plot)\r
-\r
-        figure = self.GetActiveFigure()\r
-        figure.clear()\r
-\r
-        #use '0' instead of e.g. '0.00' for scales\r
-        use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')\r
-        #optionally remove the extension from the title of the plot\r
-        hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')\r
-        if hide_curve_extension:\r
-            title = lh.remove_extension(self.displayed_plot.title)\r
-        else:\r
-            title = self.displayed_plot.title\r
-        figure.suptitle(title, fontsize=14)\r
-        #create the list of all axes necessary (rows and columns)\r
-        axes_list =[]\r
-        number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])\r
-        number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])\r
-        for index in range(number_of_rows * number_of_columns):\r
-            axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))\r
-\r
-        #add all curves to the corresponding plots\r
-        for curve in self.displayed_plot.curves:\r
-            add_to_plot(curve)\r
-\r
-        #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'\r
-        figure.subplots_adjust(hspace=0.3)\r
-\r
-        #display results\r
-        self.panelResults.ClearResults()\r
-        if self.displayed_plot.results.has_key(self.results_str):\r
-            for curve in self.displayed_plot.results[self.results_str].results:\r
-                add_to_plot(curve, set_scale=False)\r
-            self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])\r
-        else:\r
-            self.panelResults.ClearResults()\r
-        #refresh the plot\r
-        figure.canvas.draw()\r
-\r
     def _on_curve_select(self, playlist, curve):\r
         #create the plot tab and add playlist to the dictionary\r
         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))\r
@@ -692,7 +632,7 @@ class HookeFrame (wx.Frame):
         #self.select_plugin(plugin=command.plugin)\r
         if 'assistant' in self._c:\r
             self._c['assitant'].ChangeValue(command.help)\r
-        self._c['property'].clear()\r
+        self._c['property editor'].clear()\r
         for argument in command.arguments:\r
             if argument.name == 'help':\r
                 continue\r
@@ -717,7 +657,7 @@ class HookeFrame (wx.Frame):
                 argument, curves=curves, playlists=playlists)\r
             if p == None:\r
                 continue  # property intentionally not handled (yet)\r
-            self._c['property'].append_property(p)\r
+            self._c['property editor'].append_property(p)\r
 \r
         self.gui.config['selected command'] = command  # TODO: push to engine\r
 \r
@@ -824,7 +764,7 @@ class HookeFrame (wx.Frame):
         if not self._perspectives.has_key(selected_perspective):\r
             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke\r
 \r
-        self._restore_perspective(selected_perspective)\r
+        self._restore_perspective(selected_perspective, force=True)\r
         self._update_perspective_menu()\r
 \r
     def _update_perspective_menu(self):\r
@@ -863,15 +803,16 @@ class HookeFrame (wx.Frame):
         # ) that makes the radio item indicator in the menu disappear.\r
         # The code should be fine once this issue is fixed.\r
 \r
-    def _restore_perspective(self, name):\r
-        if name != self.gui.config['active perspective']:\r
+    def _restore_perspective(self, name, force=False):\r
+        if name != self.gui.config['active perspective'] or force == True:\r
             print 'restoring perspective:', name\r
             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke\r
             self._c['manager'].LoadPerspective(self._perspectives[name])\r
             self._c['manager'].Update()\r
             for pane in self._c['manager'].GetAllPanes():\r
-                if pane.name in self._c['menu bar']._c['view']._c.keys():\r
-                    pane.Check(pane.window.IsShown())\r
+                view = self._c['menu bar']._c['view']\r
+                if pane.name in view._c.keys():\r
+                    view._c[pane.name].Check(pane.window.IsShown())\r
 \r
     def _on_save_perspective(self, *args):\r
         perspective = self._c['manager'].SavePerspective()\r
@@ -1016,6 +957,18 @@ class GUI (UserInterface):
             Setting(section=self.setting_section, option='hide extensions',\r
                     value=False,\r
                     help='Hide file extensions when displaying names.'),\r
+            Setting(section=self.setting_section, option='plot legend',\r
+                    value=True,\r
+                    help='Enable/disable the plot legend.'),\r
+            Setting(section=self.setting_section, option='plot x format',\r
+                    value='None',\r
+                    help='Display format for plot x values.'),\r
+            Setting(section=self.setting_section, option='plot y format',\r
+                    value='None',\r
+                    help='Display format for plot y values.'),\r
+            Setting(section=self.setting_section, option='plot zero',\r
+                    value=0,\r
+                    help='Select "0" vs. e.g. "0.00" for plot axes?'),\r
             Setting(section=self.setting_section, option='folders-workdir',\r
                     value='.',\r
                     help='This should probably go...'),\r
index c422f9f3627260c07d2e56a63a0c01677aeccc96..068e32e625442919e9a7724f13a2bd93b90f8943 100644 (file)
@@ -6,7 +6,6 @@
 import wx
 
 from ...util.callback import callback, in_callback
-from . import panel as panel
 
 
 class Menu (wx.Menu):
@@ -79,14 +78,14 @@ class FileMenu (Menu):
 
 
 class ViewMenu (Menu):
-    def __init__(self, callbacks=None, **kwargs):
+    def __init__(self, panels, callbacks=None, **kwargs):
         super(ViewMenu, self).__init__(**kwargs)
         if callbacks == None:
             callbacks = {}
         self._callbacks = callbacks
         self._c = {}
-        for i,panelname in enumerate(sorted(panel.PANELS.keys())):
-            text = '%s\tF%d' % (panelname.capitalize(), i+5)
+        for i,panelname in enumerate(sorted([p.managed_name for p in panels])):
+            text = '%s\tF%d' % (panelname, i+5)
             self._c[panelname] = self.AppendCheckItem(id=wx.ID_ANY, text=text)
         for item in self._c.values():
             item.Check()
@@ -162,7 +161,7 @@ class HelpMenu (Menu):
 
 
 class HookeMenuBar (MenuBar):
-    def __init__(self, callbacks=None, **kwargs):
+    def __init__(self, panels, callbacks=None, **kwargs):
         super(HookeMenuBar, self).__init__(**kwargs)
         if callbacks == None:
             callbacks = {}
@@ -173,5 +172,8 @@ class HookeMenuBar (MenuBar):
         for key in ['file', 'view', 'perspective', 'help']:
             cap_key = key.capitalize()
             _class = globals()['%sMenu' % cap_key]
-            self._c[key] = _class(parent=self, callbacks=callbacks)
+            kwargs = {}
+            if key == 'view':
+                kwargs['panels'] = panels
+            self._c[key] = _class(parent=self, callbacks=callbacks, **kwargs)
             self.Append(self._c[key], cap_key)
index 4a2a8daeff0a58d4ff0fd5640e9aeca3f3e2d9f5..11e5187a640db801f9dc89308320d71a311d1591 100644 (file)
@@ -1,5 +1,8 @@
 # Copyright\r
 \r
+"""The `panel` module provides optional submodules that add GUI panels.\r
+"""\r
+\r
 from ....util.pluggable import IsSubclass, construct_odict\r
 \r
 \r
@@ -9,9 +12,8 @@ PANEL_MODULES = [
 #    'notebook',\r
     'output',\r
     'playlist',\r
-#    'plot',\r
-#    'propertyeditor',\r
-    'propertyeditor2',\r
+    'plot',\r
+    'propertyeditor',\r
 #    'results',\r
 #    'selection',\r
 #    'welcome',\r
@@ -28,6 +30,8 @@ class Panel (object):
     def __init__(self, name=None, callbacks=None, **kwargs):\r
         super(Panel, self).__init__(**kwargs)\r
         self.name = name\r
+        self.managed_name = name.capitalize()\r
+        self._hooke_frame = kwargs.get('parent', None)\r
         if callbacks == None:\r
             callbacks = {}\r
         self._callbacks = callbacks\r
index 5cbeda1b9570e5d188d59b6775203b443d8c9d0c..d9c996c6628c62a7a98af39d437fe814af26a5b5 100644 (file)
@@ -28,7 +28,9 @@ class Menu (wx.Menu):
 class Tree (wx.TreeCtrl):\r
     """:class:`wx.TreeCtrl` subclass handling playlist and curve selection.\r
     """\r
-    def __init__(self, config, callbacks, *args, **kwargs):\r
+    def __init__(self, *args, **kwargs):\r
+        self._panel = kwargs['parent']\r
+        self._callbacks = self._panel._callbacks # TODO: CallbackClass.set_callback{,s}()\r
         super(Tree, self).__init__(*args, **kwargs)\r
         imglist = wx.ImageList(width=16, height=16, mask=True, initialCount=2)\r
         imglist.Add(wx.ArtProvider.GetBitmap(\r
@@ -48,8 +50,6 @@ class Tree (wx.TreeCtrl):
         self.Bind(wx.EVT_RIGHT_DOWN, self._on_context_menu)\r
         self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_select)\r
 \r
-        self.config = config\r
-        self._callbacks = callbacks\r
         self._setup_playlists()\r
 \r
     def _setup_playlists(self):\r
@@ -61,13 +61,6 @@ class Tree (wx.TreeCtrl):
         self._id_for_name = {}  # {name: id}\r
         self._name_for_id = {}  # {id: name}\r
 \r
-    def _name(self, name):\r
-        """Cleanup names according to configured preferences.\r
-        """\r
-        if self.config['hide extensions'] == 'True':  # HACK: config should decode\r
-            name,ext = os.path.splitext(name)\r
-        return name\r
-\r
     def _is_curve(self, name):  # name from ._id_for_name / ._name_for_id\r
         """Return `True` if `name` corresponds to a :class:`hooke.curve.Curve`.\r
         """\r
@@ -128,7 +121,7 @@ class Tree (wx.TreeCtrl):
         self._playlists[playlist.name] = playlist\r
         p_id = self.AppendItem(\r
             parent=self._c['root'],\r
-            text=self._name(playlist.name),\r
+            text=self._panel._hooke_frame._file_name(playlist.name),\r
             image=self.image['playlist'])\r
         self._id_for_name[playlist.name] = p_id\r
         self._name_for_id[p_id] = playlist.name\r
@@ -154,7 +147,7 @@ class Tree (wx.TreeCtrl):
             p.append(curve)\r
         c_id = self.AppendItem(\r
             parent=self._id_for_name[playlist_name],\r
-            text=self._name(curve.name),\r
+            text=self._panel._hooke_frame._file_name(curve.name),\r
             image=self.image['curve'])\r
         self._id_for_name[(p.name, curve.name)] = c_id\r
         self._name_for_id[c_id] = (p.name, curve.name)\r
@@ -324,15 +317,12 @@ class Tree (wx.TreeCtrl):
 class Playlist (Panel, wx.Panel):\r
     """:class:`wx.Panel` subclass wrapper for :class:`Tree`.\r
     """\r
-    def __init__(self, config, callbacks, *args, **kwargs):\r
+    def __init__(self, callbacks=None, **kwargs):\r
         # Use the WANTS_CHARS style so the panel doesn't eat the Return key.\r
-        super(Playlist, self).__init__(*args, **kwargs)\r
-        self.name = 'playlist panel'\r
-\r
+        super(Playlist, self).__init__(\r
+            name='playlist', callbacks=callbacks, **kwargs)\r
         self._c = {\r
             'tree': Tree(\r
-                config=config,\r
-                callbacks=callbacks,\r
                 parent=self,\r
                 size=wx.Size(160, 250),\r
                 style=wx.TR_DEFAULT_STYLE | wx.NO_BORDER | wx.TR_HIDE_ROOT),\r
index b1c7f78232d95d3608f25e34233586186aeb74f6..2281812c0880ea3acdbbba875e8baa52cfaf5cbe 100644 (file)
 # Copyright\r
 \r
 """Plot panel for Hooke.\r
+\r
+Notes\r
+-----\r
+Originally based on `this example`_.\r
+\r
+.. _this example:\r
+  http://matplotlib.sourceforge.net/examples/user_interfaces/embedding_in_wx2.html\r
 """\r
 \r
+import matplotlib\r
+matplotlib.use('WXAgg')  # use wxpython with antigrain (agg) rendering\r
 from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas\r
-from matplotlib.backends.backend_wx import NavigationToolbar2Wx\r
+from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavToolbar\r
 from matplotlib.figure import Figure\r
-\r
 import wx\r
 \r
-# There are many comments in here from the demo app.\r
-# They should come in handy to expand the functionality in the future.\r
-\r
-class HookeCustomToolbar(NavigationToolbar2Wx):\r
-\r
-    def __init__(self, plotCanvas):\r
-        NavigationToolbar2Wx.__init__(self, plotCanvas)\r
-        # add new toolbar buttons\r
-        #glyph_file = 'resources' + os.sep + 'pipette.png'\r
-        #glyph = wx.Image(glyph_file, wx.BITMAP_TYPE_ANY).ConvertToBitmap()\r
-\r
-        #self.AddCheckTool(ON_CUSTOM_PICK, glyph, shortHelp='Select a data point', longHelp='Select a data point')\r
-        #wx.EVT_TOOL(self, ON_CUSTOM_PICK, self.OnSelectPoint)\r
-\r
-        # remove the unwanted button\r
-#        POSITION_OF_CONFIGURE_SUBPLOTS_BTN = 6\r
-#        self.DeleteToolByPos(POSITION_OF_CONFIGURE_SUBPLOTS_BTN)\r
-\r
-    #def OnSelectPoint(self, event):\r
-        #self.Parent.Parent.Parent.pick_active = True\r
-\r
-\r
-#class LineBuilder:\r
-    #def __init__(self, line):\r
-        #self.line = line\r
-        #self.xs = list(line.get_xdata())\r
-        #self.ys = list(line.get_ydata())\r
-        #self.cid = line.figure.canvas.mpl_connect('button_press_event', self)\r
-\r
-    #def __call__(self, event):\r
-        #print 'click', event\r
-        #if event.inaxes != self.line.axes:\r
-            #return\r
-        #self.xs.append(event.xdata)\r
-        #self.ys.append(event.ydata)\r
-        #self.line.set_data(self.xs, self.ys)\r
-        #self.line.figure.canvas.draw()\r
-\r
+from ....util.callback import callback, in_callback\r
+from . import Panel\r
 \r
-class PlotPanel(wx.Panel):\r
-\r
-    def __init__(self, parent, ID):\r
-        wx.Panel.__init__(self, parent, ID, style=wx.WANTS_CHARS|wx.NO_BORDER, size=(160, 200))\r
-\r
-        self.figure = Figure()\r
-        self.canvas = FigureCanvas(self, -1, self.figure)\r
-        self.SetColor(wx.NamedColor('WHITE'))\r
-\r
-        self.sizer = wx.BoxSizer(wx.VERTICAL)\r
-        self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)\r
-        self.SetSizer(self.sizer)\r
-        self.Fit()\r
 \r
+class PlotPanel (Panel, wx.Panel):\r
+    """UI for graphical curve display.\r
+    """\r
+    def __init__(self, callbacks=None, **kwargs):\r
         self.display_coordinates = False\r
+        self.style = 'line'\r
+        self._curve = None\r
+        super(PlotPanel, self).__init__(\r
+            name='plot', callbacks=callbacks, **kwargs)\r
+        self._c = {}\r
+        self._c['figure'] = Figure()\r
+        self._c['canvas'] = FigureCanvas(\r
+            parent=self, id=wx.ID_ANY, figure=self._c['figure'])\r
+        self._c['toolbar'] = NavToolbar(self._c['canvas'])\r
+\r
+        self._set_color(wx.NamedColor('WHITE'))\r
+        sizer = wx.BoxSizer(wx.VERTICAL)\r
+        sizer.Add(self._c['canvas'], 1, wx.LEFT | wx.TOP | wx.GROW)\r
+        self._setup_toolbar(toolbar=self._c['toolbar'], sizer=sizer)\r
+        self.SetSizer(sizer)\r
+        self.Fit()\r
 \r
-        self.figure.canvas.mpl_connect('button_press_event', self.OnClick)\r
-        self.figure.canvas.mpl_connect('axes_enter_event', self.OnEnterAxes)\r
-        self.figure.canvas.mpl_connect('axes_leave_event', self.OnLeaveAxes)\r
-        self.figure.canvas.mpl_connect('motion_notify_event', self.OnMouseMove)\r
-        self.add_toolbar()  # comment this out for no toolbar\r
-\r
-    def add_toolbar(self):\r
-        self.toolbar = HookeCustomToolbar(self.canvas)\r
-        self.toolbar.Realize()\r
+        self.Bind(wx.EVT_SIZE, self._on_size) \r
+        self._c['figure'].canvas.mpl_connect(\r
+            'button_press_event', self._on_click)\r
+        self._c['figure'].canvas.mpl_connect(\r
+            'axes_enter_event', self._on_enter_axes)\r
+        self._c['figure'].canvas.mpl_connect(\r
+            'axes_leave_event', self._on_leave_axes)\r
+        self._c['figure'].canvas.mpl_connect(\r
+            'motion_notify_event', self._on_mouse_move)\r
+\r
+    def _setup_toolbar(self, toolbar, sizer):\r
+        self._c['toolbar'].Realize()  # call after putting items in the toolbar\r
         if wx.Platform == '__WXMAC__':\r
             # Mac platform (OSX 10.3, MacPython) does not seem to cope with\r
             # having a toolbar in a sizer. This work-around gets the buttons\r
             # back, but at the expense of having the toolbar at the top\r
-            self.SetToolBar(self.toolbar)\r
-        else:\r
+            self.SetToolBar(toolbar)\r
+        elif wx.Platform == '__WXMSW__':\r
             # On Windows platform, default window size is incorrect, so set\r
             # toolbar width to figure width.\r
-            tw, th = self.toolbar.GetSizeTuple()\r
-            fw, fh = self.canvas.GetSizeTuple()\r
+            tw, th = toolbar.GetSizeTuple()\r
+            fw, fh = self._c['canvas'].GetSizeTuple()\r
             # By adding toolbar in sizer, we are able to put it at the bottom\r
             # of the frame - so appearance is closer to GTK version.\r
             # As noted above, doesn't work for Mac.\r
-            self.toolbar.SetSize(wx.Size(fw, th))\r
-            self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)\r
-        # update the axes menu on the toolbar\r
-        self.toolbar.update()\r
-\r
-    def get_figure(self):\r
-        return self.figure\r
-\r
-    def SetColor(self, rgbtuple):\r
-        '''\r
-        Set figure and canvas colours to be the same\r
-        '''\r
-        if not rgbtuple:\r
+            toolbar.SetSize(wx.Size(fw, th))\r
+            sizer.Add(toolbar, 0 , wx.LEFT | wx.EXPAND)\r
+        else:\r
+            sizer.Add(toolbar, 0 , wx.LEFT | wx.EXPAND)\r
+        self._c['toolbar'].update()  # update the axes menu on the toolbar\r
+\r
+    def _set_color(self, rgbtuple=None):\r
+        """Set both figure and canvas colors to `rgbtuple`.\r
+        """\r
+        if rgbtuple == None:\r
             rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()\r
-        col = [c / 255.0 for c in rgbtuple]\r
-        self.figure.set_facecolor(col)\r
-        self.figure.set_edgecolor(col)\r
-        self.canvas.SetBackgroundColour(wx.Colour(*rgbtuple))\r
+        col = [c/255.0 for c in rgbtuple]\r
+        self._c['figure'].set_facecolor(col)\r
+        self._c['figure'].set_edgecolor(col)\r
+        self._c['canvas'].SetBackgroundColour(wx.Colour(*rgbtuple))\r
 \r
-    def SetStatusText(self, text, field=1):\r
-        self.Parent.Parent.statusbar.SetStatusText(text, field)\r
+    #def SetStatusText(self, text, field=1):\r
+    #    self.Parent.Parent.statusbar.SetStatusText(text, field)\r
 \r
-    def OnClick(self, event):\r
+    def _on_size(self, event):\r
+        event.Skip()\r
+        wx.CallAfter(self._resize_canvas)\r
+\r
+    def _on_click(self, event):\r
         #self.SetStatusText(str(event.xdata))\r
         #print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%(event.button, event.x, event.y, event.xdata, event.ydata)\r
         pass\r
 \r
-    def OnEnterAxes(self, event):\r
+    def _on_enter_axes(self, event):\r
         self.display_coordinates = True\r
 \r
-    def OnLeaveAxes(self, event):\r
+    def _on_leave_axes(self, event):\r
         self.display_coordinates = False\r
-        self.SetStatusText('')\r
+        #self.SetStatusText('')\r
 \r
-    def OnMouseMove(self, event):\r
+    def _on_mouse_move(self, event):\r
         if event.guiEvent.m_shiftDown:\r
-            self.toolbar.set_cursor(2)\r
-            #print 'hand: ' + str(wx.CURSOR_HAND)\r
-            #print 'cross: ' + str(wx.CURSOR_CROSS)\r
-            #print 'ibeam: ' + str(wx.CURSOR_IBEAM)\r
-            #print 'wait: ' + str(wx.CURSOR_WAIT)\r
-            #print 'hourglass: ' + str(wx.HOURGLASS_CURSOR)\r
+            self._c['toolbar'].set_cursor(wx.CURSOR_RIGHT_ARROW)\r
         else:\r
-            self.toolbar.set_cursor(1)\r
-\r
-            #axes = self.figure.axes[0]\r
-            #line, = axes.plot([event.x - 20 , event.x + 20], [event.y - 20, event.y + 20])\r
-\r
-            #line.figure.canvas.draw()\r
+            self._c['toolbar'].set_cursor(wx.CURSOR_ARROW)\r
         if self.display_coordinates:\r
-            coordinateString = ''.join(['x: ', str(event.xdata), ' y: ', str(event.ydata)])\r
+            coordinateString = ''.join(\r
+                ['x: ', str(event.xdata), ' y: ', str(event.ydata)])\r
             #TODO: pretty format\r
-            self.SetStatusText(coordinateString)\r
+            #self.SetStatusText(coordinateString)\r
+\r
+    def _resize_canvas(self):\r
+        print 'resizing'\r
+        w,h = self.GetClientSize()\r
+        tw,th = self._c['toolbar'].GetSizeTuple()\r
+        dpi = float(self._c['figure'].get_dpi())\r
+        self._c['figure'].set_figwidth(w/dpi)\r
+        self._c['figure'].set_figheight((h-th)/dpi)\r
+        self._c['canvas'].draw()\r
+        self.Refresh()\r
 \r
     def OnPaint(self, event):\r
-        self.canvas.draw()\r
+        print 'painting'\r
+        super(PlotPanel, self).OnPaint(event)\r
+        self._c['canvas'].draw()\r
+\r
+    def set_curve(self, curve, config={}):\r
+        self._curve = curve\r
+        self.update(config=config)\r
+\r
+    def update(self, config={}):\r
+        print 'updating'\r
+        x_format = config['plot x format']\r
+        y_format = config['plot y format']\r
+        zero = config['plot zero']\r
+\r
+        self._c['figure'].clear()\r
+        self._c['figure'].suptitle(\r
+            self._hooke_frame._file_name(self._curve.name),\r
+            fontsize=12)\r
+        axes = self._c['figure'].add_subplot(1, 1, 1)\r
+\r
+#        if x_format != 'None':\r
+#            f = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)\r
+#            axes.xaxis.set_major_formatter(f)\r
+#        if y_format != 'None':\r
+#            f = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)\r
+#            axes.yaxis.set_major_formatter(f)\r
+\r
+        x_name = 'z piezo (m)'\r
+        y_name = 'deflection (m)'\r
+        #axes.set_xlabel(x_name)\r
+        #axes.set_ylabel(y_name)\r
+\r
+        self._c['figure'].hold(True)\r
+        for i,data in enumerate(self._curve.data):\r
+            axes.plot(data[:,data.info['columns'].index(x_name)],\r
+                      data[:,data.info['columns'].index(y_name)],\r
+                      '.',\r
+                      label=data.info['name'])\r
+        if config['plot legend'] == 'True':  # HACK: config should convert\r
+            axes.legend(loc='best')\r
+        self._c['canvas'].draw()\r
diff --git a/hooke/ui/gui/panel/propertyeditor-propgrid.py b/hooke/ui/gui/panel/propertyeditor-propgrid.py
new file mode 100644 (file)
index 0000000..edf0968
--- /dev/null
@@ -0,0 +1,500 @@
+# Copyright\r
+\r
+"""Property editor panel for Hooke.\r
+"""\r
+\r
+import sys\r
+import os.path\r
+\r
+import wx\r
+import wx.propgrid as wxpg\r
+\r
+# There are many comments and code fragments in here from the demo app.\r
+# They should come in handy to expand the functionality in the future.\r
+\r
+class Display (object):\r
+    property_descriptor = []\r
+    def __init__(self):\r
+        pass\r
+\r
+class ValueObject (object):\r
+    def __init__(self):\r
+        pass\r
+\r
+\r
+class IntProperty2 (wxpg.PyProperty):\r
+    """This is a simple re-implementation of wxIntProperty.\r
+    """\r
+    def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=0):\r
+        wxpg.PyProperty.__init__(self, label, name)\r
+        self.SetValue(value)\r
+\r
+    def GetClassName(self):\r
+        return "IntProperty2"\r
+\r
+    def GetEditor(self):\r
+        return "TextCtrl"\r
+\r
+    def GetValueAsString(self, flags):\r
+        return str(self.GetValue())\r
+\r
+    def PyStringToValue(self, s, flags):\r
+        try:\r
+            v = int(s)\r
+            if self.GetValue() != v:\r
+                return v\r
+        except TypeError:\r
+            if flags & wxpg.PG_REPORT_ERROR:\r
+                wx.MessageBox("Cannot convert '%s' into a number."%s, "Error")\r
+        return False\r
+\r
+    def PyIntToValue(self, v, flags):\r
+        if (self.GetValue() != v):\r
+            return v\r
+\r
+\r
+class PyFilesProperty(wxpg.PyArrayStringProperty):\r
+    def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=[]):\r
+        wxpg.PyArrayStringProperty.__init__(self, label, name, value)\r
+        self.SetValue(value)\r
+\r
+    def OnSetValue(self, v):\r
+        self.value = v\r
+        self.display = ', '.join(self.value)\r
+\r
+    def GetValueAsString(self, argFlags):\r
+        return self.display\r
+\r
+    def PyStringToValue(self, s, flags):\r
+        return [a.strip() for a in s.split(',')]\r
+\r
+    def OnEvent(self, propgrid, ctrl, event):\r
+        if event.GetEventType() == wx.wxEVT_COMMAND_BUTTON_CLICKED:\r
+            # Show dialog to select a string, call DoSetValue and\r
+            # return True, if value changed.\r
+            return True\r
+\r
+        return False\r
+\r
+\r
+class PyObjectPropertyValue:\r
+    """\\r
+    Value type of our sample PyObjectProperty. We keep a simple dash-delimited\r
+    list of string given as argument to constructor.\r
+    """\r
+    def __init__(self, s=None):\r
+        try:\r
+            self.ls = [a.strip() for a in s.split('-')]\r
+        except:\r
+            self.ls = []\r
+\r
+    def __repr__(self):\r
+        return ' - '.join(self.ls)\r
+\r
+\r
+class PyObjectProperty(wxpg.PyProperty):\r
+    """\\r
+    Another simple example. This time our value is a PyObject (NOTE: we can't\r
+    return an arbitrary python object in DoGetValue. It cannot be a simple\r
+    type such as int, bool, double, or string, nor an array or wxObject based.\r
+    Dictionary, None, or any user-specified Python object is allowed).\r
+    """\r
+    def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=None):\r
+        wxpg.PyProperty.__init__(self, label, name)\r
+        self.SetValue(value)\r
+\r
+    def GetClassName(self):\r
+        return self.__class__.__name__\r
+\r
+    def GetEditor(self):\r
+        return "TextCtrl"\r
+\r
+    def GetValueAsString(self, flags):\r
+        return repr(self.GetValue())\r
+\r
+    def PyStringToValue(self, s, flags):\r
+        return PyObjectPropertyValue(s)\r
+\r
+\r
+class ShapeProperty(wxpg.PyEnumProperty):\r
+    """\\r
+    Demonstrates use of OnCustomPaint method.\r
+    """\r
+    def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=-1):\r
+        wxpg.PyEnumProperty.__init__(self, label, name, ['Line','Circle','Rectangle'], [0,1,2], value)\r
+\r
+    def OnMeasureImage(self, index):\r
+        return wxpg.DEFAULT_IMAGE_SIZE\r
+\r
+    def OnCustomPaint(self, dc, rect, paint_data):\r
+        """\\r
+        paint_data.m_choiceItem is -1 if we are painting the control,\r
+        in which case we need to get the drawn item using DoGetValue.\r
+        """\r
+        item = paint_data.m_choiceItem\r
+        if item == -1:\r
+            item = self.DoGetValue()\r
+\r
+        dc.SetPen(wx.Pen(wx.BLACK))\r
+        dc.SetBrush(wx.Brush(wx.BLACK))\r
+\r
+        if item == 0:\r
+            dc.DrawLine(rect.x,rect.y,rect.x+rect.width,rect.y+rect.height)\r
+        elif item == 1:\r
+            half_width = rect.width / 2\r
+            dc.DrawCircle(rect.x+half_width,rect.y+half_width,half_width-3)\r
+        elif item == 2:\r
+            dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)\r
+\r
+\r
+class LargeImagePickerCtrl(wx.Window):\r
+    """\\r
+    Control created and used by LargeImageEditor.\r
+    """\r
+    def __init__(self):\r
+        pre = wx.PreWindow()\r
+        self.PostCreate(pre)\r
+\r
+    def Create(self, parent, id_, pos, size, style = 0):\r
+        wx.Window.Create(self, parent, id_, pos, size, style | wx.BORDER_SIMPLE)\r
+        img_spc = size[1]\r
+        self.tc = wx.TextCtrl(self, -1, "", (img_spc,0), (2048,size[1]), wx.BORDER_NONE)\r
+        self.SetBackgroundColour(wx.WHITE)\r
+        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)\r
+        self.property = None\r
+        self.bmp = None\r
+        self.Bind(wx.EVT_PAINT, self.OnPaint)\r
+\r
+    def OnPaint(self, event):\r
+        dc = wx.BufferedPaintDC(self)\r
+\r
+        whiteBrush = wx.Brush(wx.WHITE)\r
+        dc.SetBackground(whiteBrush)\r
+        dc.Clear()\r
+\r
+        bmp = self.bmp\r
+        if bmp:\r
+            dc.DrawBitmap(bmp, 2, 2)\r
+        else:\r
+            dc.SetPen(wx.Pen(wx.BLACK))\r
+            dc.SetBrush(whiteBrush)\r
+            dc.DrawRectangle(2, 2, 64, 64)\r
+\r
+    def RefreshThumbnail(self):\r
+        """\\r
+        We use here very simple image scaling code.\r
+        """\r
+        if not self.property:\r
+            self.bmp = None\r
+            return\r
+\r
+        path = self.property.DoGetValue()\r
+\r
+        if not os.path.isfile(path):\r
+            self.bmp = None\r
+            return\r
+\r
+        image = wx.Image(path)\r
+        image.Rescale(64, 64)\r
+        self.bmp = wx.BitmapFromImage(image)\r
+\r
+    def SetProperty(self, property):\r
+        self.property = property\r
+        self.tc.SetValue(property.GetDisplayedString())\r
+        self.RefreshThumbnail()\r
+\r
+    def SetValue(self, s):\r
+        self.RefreshThumbnail()\r
+        self.tc.SetValue(s)\r
+\r
+    def GetLastPosition(self):\r
+        return self.tc.GetLastPosition()\r
+\r
+\r
+class LargeImageEditor(wxpg.PyEditor):\r
+    """\\r
+    Double-height text-editor with image in front.\r
+    """\r
+    def __init__(self):\r
+        wxpg.PyEditor.__init__(self)\r
+\r
+    def CreateControls(self, propgrid, property, pos, sz):\r
+        try:\r
+            h = 64 + 6\r
+            x = propgrid.GetSplitterPosition()\r
+            x2 = propgrid.GetClientSize().x\r
+            bw = propgrid.GetRowHeight()\r
+            lipc = LargeImagePickerCtrl()\r
+            if sys.platform == 'win32':\r
+                lipc.Hide()\r
+            lipc.Create(propgrid, wxpg.PG_SUBID1, (x,pos[1]), (x2-x-bw,h))\r
+            lipc.SetProperty(property)\r
+            # Hmmm.. how to have two-stage creation without subclassing?\r
+            #btn = wx.PreButton()\r
+            #pre = wx.PreWindow()\r
+            #self.PostCreate(pre)\r
+            #if sys.platform == 'win32':\r
+            #    btn.Hide()\r
+            #btn.Create(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS)\r
+            btn = wx.Button(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS)\r
+            return (lipc, btn)\r
+        except:\r
+            import traceback\r
+            print traceback.print_exc()\r
+\r
+    def UpdateControl(self, property, ctrl):\r
+        ctrl.SetValue(property.GetDisplayedString())\r
+\r
+    def DrawValue(self, dc, property, rect):\r
+        if not (property.GetFlags() & wxpg.PG_PROP_AUTO_UNSPECIFIED):\r
+            dc.DrawText( property.GetDisplayedString(), rect.x+5, rect.y );\r
+\r
+    def OnEvent(self, propgrid, ctrl, event):\r
+        if not ctrl:\r
+            return False\r
+\r
+        evtType = event.GetEventType()\r
+\r
+        if evtType == wx.wxEVT_COMMAND_TEXT_ENTER:\r
+            if propgrid.IsEditorsValueModified():\r
+                return True\r
+\r
+        elif evtType == wx.wxEVT_COMMAND_TEXT_UPDATED:\r
+            if not property.HasFlag(wxpg.PG_PROP_AUTO_UNSPECIFIED) or not ctrl or \\r
+               ctrl.GetLastPosition() > 0:\r
+\r
+                # We must check this since an 'empty' text event\r
+                # may be triggered when creating the property.\r
+                PG_FL_IN_SELECT_PROPERTY = 0x00100000\r
+                if not (propgrid.GetInternalFlags() & PG_FL_IN_SELECT_PROPERTY):\r
+                    event.Skip();\r
+                    event.SetId(propgrid.GetId());\r
+\r
+                propgrid.EditorsValueWasModified();\r
+\r
+        return False\r
+\r
+\r
+    def CopyValueFromControl(self, property, ctrl):\r
+        tc = ctrl.tc\r
+        res = property.SetValueFromString(tc.GetValue(),0)\r
+        # Changing unspecified always causes event (returning\r
+        # true here should be enough to trigger it).\r
+        if not res and property.IsFlagSet(wxpg.PG_PROP_AUTO_UNSPECIFIED):\r
+            res = True\r
+\r
+        return res\r
+\r
+    def SetValueToUnspecified(self, ctrl):\r
+        ctrl.tc.Remove(0,len(ctrl.tc.GetValue()));\r
+\r
+    def SetControlStringValue(self, ctrl, txt):\r
+        ctrl.SetValue(txt)\r
+\r
+    def OnFocus(self, property, ctrl):\r
+        ctrl.tc.SetSelection(-1,-1)\r
+        ctrl.tc.SetFocus()\r
+\r
+\r
+class PropertyEditor(wx.Panel):\r
+\r
+    def __init__(self, parent):\r
+        # Use the WANTS_CHARS style so the panel doesn't eat the Return key.\r
+        wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS, size=(160, 200))\r
+\r
+        sizer = wx.BoxSizer(wx.VERTICAL)\r
+\r
+        self.pg = wxpg.PropertyGrid(self, style=wxpg.PG_SPLITTER_AUTO_CENTER|wxpg.PG_AUTO_SORT)\r
+\r
+        # Show help as tooltips\r
+        self.pg.SetExtraStyle(wxpg.PG_EX_HELP_AS_TOOLTIPS)\r
+\r
+        #pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChange)\r
+        #pg.Bind(wxpg.EVT_PG_SELECTED, self.OnPropGridSelect)\r
+        #self.pg.Bind(wxpg.EVT_PG_RIGHT_CLICK, self.OnPropGridRightClick)\r
+\r
+        # Needed by custom image editor\r
+        wx.InitAllImageHandlers()\r
+\r
+        #\r
+        # Let's create a simple custom editor\r
+        #\r
+        # NOTE: Editor must be registered *before* adding a property that uses it.\r
+        self.pg.RegisterEditor(LargeImageEditor)\r
+\r
+        '''\r
+        #\r
+        # Add properties\r
+        #\r
+\r
+        pg.Append( wxpg.PropertyCategory("1 - Basic Properties") )\r
+        pg.Append( wxpg.StringProperty("String",value="Some Text") )\r
+        pg.Append( wxpg.IntProperty("Int",value=100) )\r
+        pg.Append( wxpg.FloatProperty("Float",value=100.0) )\r
+        pg.Append( wxpg.BoolProperty("Bool",value=True) )\r
+        pg.Append( wxpg.BoolProperty("Bool_with_Checkbox",value=True) )\r
+        pg.SetPropertyAttribute("Bool_with_Checkbox", "UseCheckbox", True)\r
+\r
+        pg.Append( wxpg.PropertyCategory("2 - More Properties") )\r
+        pg.Append( wxpg.LongStringProperty("LongString",value="This is a\\nmulti-line string\\nwith\\ttabs\\nmixed\\tin.") )\r
+        pg.Append( wxpg.DirProperty("Dir",value="C:\\Windows") )\r
+        pg.Append( wxpg.FileProperty("File",value="C:\\Windows\\system.ini") )\r
+        pg.Append( wxpg.ArrayStringProperty("ArrayString",value=['A','B','C']) )\r
+\r
+        pg.Append( wxpg.EnumProperty("Enum","Enum",\r
+                                     ['wxPython Rules','wxPython Rocks','wxPython Is The Best'],\r
+                                     [10,11,12],0) )\r
+        pg.Append( wxpg.EditEnumProperty("EditEnum","EditEnumProperty",['A','B','C'],[0,1,2],"Text Not in List") )\r
+\r
+        pg.Append( wxpg.PropertyCategory("3 - Advanced Properties") )\r
+        pg.Append( wxpg.DateProperty("Date",value=wx.DateTime_Now()) )\r
+        pg.Append( wxpg.FontProperty("Font",value=self.GetFont()) )\r
+        pg.Append( wxpg.ColourProperty("Colour",value=self.GetBackgroundColour()) )\r
+        pg.Append( wxpg.SystemColourProperty("SystemColour") )\r
+        pg.Append( wxpg.ImageFileProperty("ImageFile") )\r
+        pg.Append( wxpg.MultiChoiceProperty("MultiChoice",choices=['wxWidgets','QT','GTK+']) )\r
+\r
+        pg.Append( wxpg.PropertyCategory("4 - Additional Properties") )\r
+        pg.Append( wxpg.PointProperty("Point",value=self.GetPosition()) )\r
+        pg.Append( wxpg.SizeProperty("Size",value=self.GetSize()) )\r
+        pg.Append( wxpg.FontDataProperty("FontData") )\r
+        pg.Append( wxpg.IntProperty("IntWithSpin",value=256) )\r
+        pg.SetPropertyEditor("IntWithSpin","SpinCtrl")\r
+        pg.Append( wxpg.DirsProperty("Dirs",value=['C:/Lib','C:/Bin']) )\r
+        pg.SetPropertyHelpString( "String", "String Property help string!" )\r
+        pg.SetPropertyHelpString( "Dirs", "Dirs Property help string!" )\r
+\r
+        pg.SetPropertyAttribute( "File", wxpg.PG_FILE_SHOW_FULL_PATH, 0 )\r
+        pg.SetPropertyAttribute( "File", wxpg.PG_FILE_INITIAL_PATH, "C:\\Program Files\\Internet Explorer" )\r
+        pg.SetPropertyAttribute( "Date", wxpg.PG_DATE_PICKER_STYLE, wx.DP_DROPDOWN|wx.DP_SHOWCENTURY )\r
+\r
+        pg.Append( wxpg.PropertyCategory("5 - Custom Properties") )\r
+        pg.Append( IntProperty2("IntProperty2", value=1024) )\r
+\r
+        pg.Append( ShapeProperty("ShapeProperty", value=0) )\r
+        pg.Append( PyObjectProperty("PyObjectProperty") )\r
+\r
+        pg.Append( wxpg.ImageFileProperty("ImageFileWithLargeEditor") )\r
+        pg.SetPropertyEditor("ImageFileWithLargeEditor", "LargeImageEditor")\r
+\r
+\r
+        pg.SetPropertyClientData( "Point", 1234 )\r
+        if pg.GetPropertyClientData( "Point" ) != 1234:\r
+            raise ValueError("Set/GetPropertyClientData() failed")\r
+\r
+        # Test setting unicode string\r
+        pg.GetPropertyByName("String").SetValue(u"Some Unicode Text")\r
+\r
+        #\r
+        # Test some code that *should* fail (but not crash)\r
+        #try:\r
+            #a_ = pg.GetPropertyValue( "NotARealProperty" )\r
+            #pg.EnableProperty( "NotAtAllRealProperty", False )\r
+            #pg.SetPropertyHelpString( "AgaintNotARealProperty", "Dummy Help String" )\r
+        #except:\r
+            #pass\r
+            #raise\r
+\r
+        '''\r
+        sizer.Add(self.pg, 1, wx.EXPAND)\r
+        self.SetSizer(sizer)\r
+        sizer.SetSizeHints(self)\r
+\r
+        self.SelectedTreeItem = None\r
+\r
+    def GetPropertyValues(self):\r
+        return self.pg.GetPropertyValues()\r
+\r
+    def Initialize(self, properties):\r
+        pg = self.pg\r
+        pg.Clear()\r
+\r
+        if properties:\r
+            for element in properties:\r
+                if element[1]['type'] == 'arraystring':\r
+                    elements = element[1]['elements']\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1]['value']\r
+                    else:\r
+                        property_value = element[1]['default']\r
+                    #retrieve individual strings\r
+                    property_value = split(property_value, ' ')\r
+                    #remove " delimiters\r
+                    values = [value.strip('"') for value in property_value]\r
+                    pg.Append(wxpg.ArrayStringProperty(element[0], value=values))\r
+\r
+                if element[1]['type'] == 'boolean':\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1].as_bool('value')\r
+                    else:\r
+                        property_value = element[1].as_bool('default')\r
+                    property_control = wxpg.BoolProperty(element[0], value=property_value)\r
+                    pg.Append(property_control)\r
+                    pg.SetPropertyAttribute(element[0], 'UseCheckbox', True)\r
+\r
+                #if element[0] == 'category':\r
+                    #pg.Append(wxpg.PropertyCategory(element[1]))\r
+\r
+                if element[1]['type'] == 'color':\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1]['value']\r
+                    else:\r
+                        property_value = element[1]['default']\r
+                    property_value = eval(property_value)\r
+                    pg.Append(wxpg.ColourProperty(element[0], value=property_value))\r
+\r
+                if element[1]['type'] == 'enum':\r
+                    elements = element[1]['elements']\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1]['value']\r
+                    else:\r
+                        property_value = element[1]['default']\r
+                    pg.Append(wxpg.EnumProperty(element[0], element[0], elements, [], elements.index(property_value)))\r
+\r
+                if element[1]['type'] == 'filename':\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1]['value']\r
+                    else:\r
+                        property_value = element[1]['default']\r
+                    pg.Append(wxpg.FileProperty(element[0], value=property_value))\r
+\r
+                if element[1]['type'] == 'float':\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1].as_float('value')\r
+                    else:\r
+                        property_value = element[1].as_float('default')\r
+                    property_control = wxpg.FloatProperty(element[0], value=property_value)\r
+                    pg.Append(property_control)\r
+\r
+                if element[1]['type'] == 'folder':\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1]['value']\r
+                    else:\r
+                        property_value = element[1]['default']\r
+                    pg.Append(wxpg.DirProperty(element[0], value=property_value))\r
+\r
+                if element[1]['type'] == 'integer':\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1].as_int('value')\r
+                    else:\r
+                        property_value = element[1].as_int('default')\r
+                    property_control = wxpg.IntProperty(element[0], value=property_value)\r
+                    if 'maximum' in element[1]:\r
+                        property_control.SetAttribute('Max', element[1].as_int('maximum'))\r
+                    if 'minimum' in element[1]:\r
+                        property_control.SetAttribute('Min', element[1].as_int('minimum'))\r
+                    property_control.SetAttribute('Wrap', True)\r
+                    pg.Append(property_control)\r
+                    pg.SetPropertyEditor(element[0], 'SpinCtrl')\r
+\r
+                if element[1]['type'] == 'string':\r
+                    if 'value' in element[1]:\r
+                        property_value = element[1]['value']\r
+                    else:\r
+                        property_value = element[1]['default']\r
+                    pg.Append(wxpg.StringProperty(element[0], value=property_value))\r
+\r
+        pg.Refresh()\r
+\r
+    def OnReserved(self, event):\r
+        pass\r
index edf0968ab8ace51886c548f0530683874dbd51d7..2d22e45775e2aa9cdafb7d1c7d5691add744ea62 100644 (file)
 # Copyright\r
 \r
 """Property editor panel for Hooke.\r
-"""\r
 \r
-import sys\r
-import os.path\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
-import wx\r
-import wx.propgrid as wxpg\r
+This module hacks together a workaround to be used until 2.9.1 is\r
+widely installed (or at least released ;).\r
 \r
-# There are many comments and code fragments in here from the demo app.\r
-# They should come in handy to expand the functionality in the future.\r
+.. _included:\r
+  http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download\r
+"""\r
 \r
-class Display (object):\r
-    property_descriptor = []\r
-    def __init__(self):\r
-        pass\r
+import wx.grid\r
 \r
-class ValueObject (object):\r
-    def __init__(self):\r
-        pass\r
+from . import Panel\r
 \r
 \r
-class IntProperty2 (wxpg.PyProperty):\r
-    """This is a simple re-implementation of wxIntProperty.\r
+def prop_from_argument(argument, curves=None, playlists=None):\r
+    """Convert a :class:`~hooke.command.Argument` to a :class:`Property`.\r
     """\r
-    def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=0):\r
-        wxpg.PyProperty.__init__(self, label, name)\r
-        self.SetValue(value)\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 GetClassName(self):\r
-        return "IntProperty2"\r
+def prop_from_setting(setting):\r
+    """Convert a :class:`~hooke.config.Setting` to a :class:`Property`.\r
+    """    \r
+    raise NotImplementedError()\r
 \r
-    def GetEditor(self):\r
-        return "TextCtrl"\r
 \r
-    def GetValueAsString(self, flags):\r
-        return str(self.GetValue())\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 PyStringToValue(self, s, flags):\r
-        try:\r
-            v = int(s)\r
-            if self.GetValue() != v:\r
-                return v\r
-        except TypeError:\r
-            if flags & wxpg.PG_REPORT_ERROR:\r
-                wx.MessageBox("Cannot convert '%s' into a number."%s, "Error")\r
-        return False\r
+    def get_editor(self):\r
+        """Return a suitable grid editor.\r
+        """\r
+        raise NotImplementedError()\r
 \r
-    def PyIntToValue(self, v, flags):\r
-        if (self.GetValue() != v):\r
-            return v\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
-class PyFilesProperty(wxpg.PyArrayStringProperty):\r
-    def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=[]):\r
-        wxpg.PyArrayStringProperty.__init__(self, label, name, value)\r
-        self.SetValue(value)\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 OnSetValue(self, v):\r
-        self.value = v\r
-        self.display = ', '.join(self.value)\r
+    def value_for_string(self, string):\r
+        """Return the value represented by `string`.\r
+        """\r
+        return string\r
 \r
-    def GetValueAsString(self, argFlags):\r
-        return self.display\r
 \r
-    def PyStringToValue(self, s, flags):\r
-        return [a.strip() for a in s.split(',')]\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 OnEvent(self, propgrid, ctrl, event):\r
-        if event.GetEventType() == wx.wxEVT_COMMAND_BUTTON_CLICKED:\r
-            # Show dialog to select a string, call DoSetValue and\r
-            # return True, if value changed.\r
-            return True\r
+    def get_editor(self):\r
+        return wx.grid.GridCellTextEditor()\r
 \r
-        return False\r
+    def get_renderer(self):\r
+        return wx.grid.GridCellStringRenderer()\r
 \r
 \r
-class PyObjectPropertyValue:\r
-    """\\r
-    Value type of our sample PyObjectProperty. We keep a simple dash-delimited\r
-    list of string given as argument to constructor.\r
-    """\r
-    def __init__(self, s=None):\r
-        try:\r
-            self.ls = [a.strip() for a in s.split('-')]\r
-        except:\r
-            self.ls = []\r
-\r
-    def __repr__(self):\r
-        return ' - '.join(self.ls)\r
-\r
-\r
-class PyObjectProperty(wxpg.PyProperty):\r
-    """\\r
-    Another simple example. This time our value is a PyObject (NOTE: we can't\r
-    return an arbitrary python object in DoGetValue. It cannot be a simple\r
-    type such as int, bool, double, or string, nor an array or wxObject based.\r
-    Dictionary, None, or any user-specified Python object is allowed).\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, label, name = wxpg.LABEL_AS_NAME, value=None):\r
-        wxpg.PyProperty.__init__(self, label, name)\r
-        self.SetValue(value)\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 GetClassName(self):\r
-        return self.__class__.__name__\r
+    def get_editor(self):\r
+        return wx.grid.GridCellBoolEditor()\r
 \r
-    def GetEditor(self):\r
-        return "TextCtrl"\r
+    def get_renderer(self):\r
+        return wx.grid.GridCellBoolRenderer()\r
 \r
-    def GetValueAsString(self, flags):\r
-        return repr(self.GetValue())\r
+    def string_for_value(self, value):\r
+        if value == True:\r
+            return '1'\r
+        return ''\r
 \r
-    def PyStringToValue(self, s, flags):\r
-        return PyObjectPropertyValue(s)\r
+    def value_for_string(self, string):\r
+        return string == '1'\r
 \r
 \r
-class ShapeProperty(wxpg.PyEnumProperty):\r
-    """\\r
-    Demonstrates use of OnCustomPaint method.\r
-    """\r
-    def __init__(self, label, name = wxpg.LABEL_AS_NAME, value=-1):\r
-        wxpg.PyEnumProperty.__init__(self, label, name, ['Line','Circle','Rectangle'], [0,1,2], value)\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 OnMeasureImage(self, index):\r
-        return wxpg.DEFAULT_IMAGE_SIZE\r
+    def get_editor(self):\r
+        return wx.grid.GridCellNumberEditor()\r
 \r
-    def OnCustomPaint(self, dc, rect, paint_data):\r
-        """\\r
-        paint_data.m_choiceItem is -1 if we are painting the control,\r
-        in which case we need to get the drawn item using DoGetValue.\r
-        """\r
-        item = paint_data.m_choiceItem\r
-        if item == -1:\r
-            item = self.DoGetValue()\r
+    def get_renderer(self):\r
+        return wx.grid.GridCellNumberRenderer()\r
 \r
-        dc.SetPen(wx.Pen(wx.BLACK))\r
-        dc.SetBrush(wx.Brush(wx.BLACK))\r
+    def value_for_string(self, string):\r
+        return int(string)\r
 \r
-        if item == 0:\r
-            dc.DrawLine(rect.x,rect.y,rect.x+rect.width,rect.y+rect.height)\r
-        elif item == 1:\r
-            half_width = rect.width / 2\r
-            dc.DrawCircle(rect.x+half_width,rect.y+half_width,half_width-3)\r
-        elif item == 2:\r
-            dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)\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
-class LargeImagePickerCtrl(wx.Window):\r
-    """\\r
-    Control created and used by LargeImageEditor.\r
-    """\r
-    def __init__(self):\r
-        pre = wx.PreWindow()\r
-        self.PostCreate(pre)\r
-\r
-    def Create(self, parent, id_, pos, size, style = 0):\r
-        wx.Window.Create(self, parent, id_, pos, size, style | wx.BORDER_SIMPLE)\r
-        img_spc = size[1]\r
-        self.tc = wx.TextCtrl(self, -1, "", (img_spc,0), (2048,size[1]), wx.BORDER_NONE)\r
-        self.SetBackgroundColour(wx.WHITE)\r
-        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)\r
-        self.property = None\r
-        self.bmp = None\r
-        self.Bind(wx.EVT_PAINT, self.OnPaint)\r
-\r
-    def OnPaint(self, event):\r
-        dc = wx.BufferedPaintDC(self)\r
-\r
-        whiteBrush = wx.Brush(wx.WHITE)\r
-        dc.SetBackground(whiteBrush)\r
-        dc.Clear()\r
-\r
-        bmp = self.bmp\r
-        if bmp:\r
-            dc.DrawBitmap(bmp, 2, 2)\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
-            dc.SetPen(wx.Pen(wx.BLACK))\r
-            dc.SetBrush(whiteBrush)\r
-            dc.DrawRectangle(2, 2, 64, 64)\r
+            kwargs['default'] = choices[0]\r
+        super(ChoiceProperty, self).__init__(type='choice', **kwargs)\r
+        self._choices = choices\r
 \r
-    def RefreshThumbnail(self):\r
-        """\\r
-        We use here very simple image scaling code.\r
-        """\r
-        if not self.property:\r
-            self.bmp = None\r
-            return\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
-        path = self.property.DoGetValue()\r
+    def get_renderer(self):\r
+        return None\r
+        #return wx.grid.GridCellChoiceRenderer()\r
 \r
-        if not os.path.isfile(path):\r
-            self.bmp = None\r
-            return\r
+    def string_for_value(self, value):\r
+        if hasattr(value, 'name'):\r
+            return value.name\r
+        return str(value)\r
 \r
-        image = wx.Image(path)\r
-        image.Rescale(64, 64)\r
-        self.bmp = wx.BitmapFromImage(image)\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
-    def SetProperty(self, property):\r
-        self.property = property\r
-        self.tc.SetValue(property.GetDisplayedString())\r
-        self.RefreshThumbnail()\r
 \r
-    def SetValue(self, s):\r
-        self.RefreshThumbnail()\r
-        self.tc.SetValue(s)\r
+class PathProperty (StringProperty):\r
+    """Simple file or path property.\r
 \r
-    def GetLastPosition(self):\r
-        return self.tc.GetLastPosition()\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 LargeImageEditor(wxpg.PyEditor):\r
-    """\\r
-    Double-height text-editor with image in front.\r
+class PropertyPanel(Panel, wx.grid.Grid):\r
+    """UI to view/set config values and command argsuments.\r
     """\r
-    def __init__(self):\r
-        wxpg.PyEditor.__init__(self)\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
-    def CreateControls(self, propgrid, property, pos, sz):\r
-        try:\r
-            h = 64 + 6\r
-            x = propgrid.GetSplitterPosition()\r
-            x2 = propgrid.GetClientSize().x\r
-            bw = propgrid.GetRowHeight()\r
-            lipc = LargeImagePickerCtrl()\r
-            if sys.platform == 'win32':\r
-                lipc.Hide()\r
-            lipc.Create(propgrid, wxpg.PG_SUBID1, (x,pos[1]), (x2-x-bw,h))\r
-            lipc.SetProperty(property)\r
-            # Hmmm.. how to have two-stage creation without subclassing?\r
-            #btn = wx.PreButton()\r
-            #pre = wx.PreWindow()\r
-            #self.PostCreate(pre)\r
-            #if sys.platform == 'win32':\r
-            #    btn.Hide()\r
-            #btn.Create(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS)\r
-            btn = wx.Button(propgrid, wxpg.PG_SUBID2, '...', (x2-bw,pos[1]), (bw,h), wx.WANTS_CHARS)\r
-            return (lipc, btn)\r
-        except:\r
-            import traceback\r
-            print traceback.print_exc()\r
-\r
-    def UpdateControl(self, property, ctrl):\r
-        ctrl.SetValue(property.GetDisplayedString())\r
-\r
-    def DrawValue(self, dc, property, rect):\r
-        if not (property.GetFlags() & wxpg.PG_PROP_AUTO_UNSPECIFIED):\r
-            dc.DrawText( property.GetDisplayedString(), rect.x+5, rect.y );\r
-\r
-    def OnEvent(self, propgrid, ctrl, event):\r
-        if not ctrl:\r
-            return False\r
-\r
-        evtType = event.GetEventType()\r
-\r
-        if evtType == wx.wxEVT_COMMAND_TEXT_ENTER:\r
-            if propgrid.IsEditorsValueModified():\r
-                return True\r
-\r
-        elif evtType == wx.wxEVT_COMMAND_TEXT_UPDATED:\r
-            if not property.HasFlag(wxpg.PG_PROP_AUTO_UNSPECIFIED) or not ctrl or \\r
-               ctrl.GetLastPosition() > 0:\r
-\r
-                # We must check this since an 'empty' text event\r
-                # may be triggered when creating the property.\r
-                PG_FL_IN_SELECT_PROPERTY = 0x00100000\r
-                if not (propgrid.GetInternalFlags() & PG_FL_IN_SELECT_PROPERTY):\r
-                    event.Skip();\r
-                    event.SetId(propgrid.GetId());\r
-\r
-                propgrid.EditorsValueWasModified();\r
-\r
-        return False\r
-\r
-\r
-    def CopyValueFromControl(self, property, ctrl):\r
-        tc = ctrl.tc\r
-        res = property.SetValueFromString(tc.GetValue(),0)\r
-        # Changing unspecified always causes event (returning\r
-        # true here should be enough to trigger it).\r
-        if not res and property.IsFlagSet(wxpg.PG_PROP_AUTO_UNSPECIFIED):\r
-            res = True\r
-\r
-        return res\r
-\r
-    def SetValueToUnspecified(self, ctrl):\r
-        ctrl.tc.Remove(0,len(ctrl.tc.GetValue()));\r
-\r
-    def SetControlStringValue(self, ctrl, txt):\r
-        ctrl.SetValue(txt)\r
-\r
-    def OnFocus(self, property, ctrl):\r
-        ctrl.tc.SetSelection(-1,-1)\r
-        ctrl.tc.SetFocus()\r
-\r
-\r
-class PropertyEditor(wx.Panel):\r
-\r
-    def __init__(self, parent):\r
-        # Use the WANTS_CHARS style so the panel doesn't eat the Return key.\r
-        wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS, size=(160, 200))\r
-\r
-        sizer = wx.BoxSizer(wx.VERTICAL)\r
-\r
-        self.pg = wxpg.PropertyGrid(self, style=wxpg.PG_SPLITTER_AUTO_CENTER|wxpg.PG_AUTO_SORT)\r
-\r
-        # Show help as tooltips\r
-        self.pg.SetExtraStyle(wxpg.PG_EX_HELP_AS_TOOLTIPS)\r
-\r
-        #pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChange)\r
-        #pg.Bind(wxpg.EVT_PG_SELECTED, self.OnPropGridSelect)\r
-        #self.pg.Bind(wxpg.EVT_PG_RIGHT_CLICK, self.OnPropGridRightClick)\r
-\r
-        # Needed by custom image editor\r
-        wx.InitAllImageHandlers()\r
-\r
-        #\r
-        # Let's create a simple custom editor\r
-        #\r
-        # NOTE: Editor must be registered *before* adding a property that uses it.\r
-        self.pg.RegisterEditor(LargeImageEditor)\r
-\r
-        '''\r
-        #\r
-        # Add properties\r
-        #\r
-\r
-        pg.Append( wxpg.PropertyCategory("1 - Basic Properties") )\r
-        pg.Append( wxpg.StringProperty("String",value="Some Text") )\r
-        pg.Append( wxpg.IntProperty("Int",value=100) )\r
-        pg.Append( wxpg.FloatProperty("Float",value=100.0) )\r
-        pg.Append( wxpg.BoolProperty("Bool",value=True) )\r
-        pg.Append( wxpg.BoolProperty("Bool_with_Checkbox",value=True) )\r
-        pg.SetPropertyAttribute("Bool_with_Checkbox", "UseCheckbox", True)\r
-\r
-        pg.Append( wxpg.PropertyCategory("2 - More Properties") )\r
-        pg.Append( wxpg.LongStringProperty("LongString",value="This is a\\nmulti-line string\\nwith\\ttabs\\nmixed\\tin.") )\r
-        pg.Append( wxpg.DirProperty("Dir",value="C:\\Windows") )\r
-        pg.Append( wxpg.FileProperty("File",value="C:\\Windows\\system.ini") )\r
-        pg.Append( wxpg.ArrayStringProperty("ArrayString",value=['A','B','C']) )\r
-\r
-        pg.Append( wxpg.EnumProperty("Enum","Enum",\r
-                                     ['wxPython Rules','wxPython Rocks','wxPython Is The Best'],\r
-                                     [10,11,12],0) )\r
-        pg.Append( wxpg.EditEnumProperty("EditEnum","EditEnumProperty",['A','B','C'],[0,1,2],"Text Not in List") )\r
-\r
-        pg.Append( wxpg.PropertyCategory("3 - Advanced Properties") )\r
-        pg.Append( wxpg.DateProperty("Date",value=wx.DateTime_Now()) )\r
-        pg.Append( wxpg.FontProperty("Font",value=self.GetFont()) )\r
-        pg.Append( wxpg.ColourProperty("Colour",value=self.GetBackgroundColour()) )\r
-        pg.Append( wxpg.SystemColourProperty("SystemColour") )\r
-        pg.Append( wxpg.ImageFileProperty("ImageFile") )\r
-        pg.Append( wxpg.MultiChoiceProperty("MultiChoice",choices=['wxWidgets','QT','GTK+']) )\r
-\r
-        pg.Append( wxpg.PropertyCategory("4 - Additional Properties") )\r
-        pg.Append( wxpg.PointProperty("Point",value=self.GetPosition()) )\r
-        pg.Append( wxpg.SizeProperty("Size",value=self.GetSize()) )\r
-        pg.Append( wxpg.FontDataProperty("FontData") )\r
-        pg.Append( wxpg.IntProperty("IntWithSpin",value=256) )\r
-        pg.SetPropertyEditor("IntWithSpin","SpinCtrl")\r
-        pg.Append( wxpg.DirsProperty("Dirs",value=['C:/Lib','C:/Bin']) )\r
-        pg.SetPropertyHelpString( "String", "String Property help string!" )\r
-        pg.SetPropertyHelpString( "Dirs", "Dirs Property help string!" )\r
-\r
-        pg.SetPropertyAttribute( "File", wxpg.PG_FILE_SHOW_FULL_PATH, 0 )\r
-        pg.SetPropertyAttribute( "File", wxpg.PG_FILE_INITIAL_PATH, "C:\\Program Files\\Internet Explorer" )\r
-        pg.SetPropertyAttribute( "Date", wxpg.PG_DATE_PICKER_STYLE, wx.DP_DROPDOWN|wx.DP_SHOWCENTURY )\r
-\r
-        pg.Append( wxpg.PropertyCategory("5 - Custom Properties") )\r
-        pg.Append( IntProperty2("IntProperty2", value=1024) )\r
-\r
-        pg.Append( ShapeProperty("ShapeProperty", value=0) )\r
-        pg.Append( PyObjectProperty("PyObjectProperty") )\r
-\r
-        pg.Append( wxpg.ImageFileProperty("ImageFileWithLargeEditor") )\r
-        pg.SetPropertyEditor("ImageFileWithLargeEditor", "LargeImageEditor")\r
-\r
-\r
-        pg.SetPropertyClientData( "Point", 1234 )\r
-        if pg.GetPropertyClientData( "Point" ) != 1234:\r
-            raise ValueError("Set/GetPropertyClientData() failed")\r
-\r
-        # Test setting unicode string\r
-        pg.GetPropertyByName("String").SetValue(u"Some Unicode Text")\r
-\r
-        #\r
-        # Test some code that *should* fail (but not crash)\r
-        #try:\r
-            #a_ = pg.GetPropertyValue( "NotARealProperty" )\r
-            #pg.EnableProperty( "NotAtAllRealProperty", False )\r
-            #pg.SetPropertyHelpString( "AgaintNotARealProperty", "Dummy Help String" )\r
-        #except:\r
-            #pass\r
-            #raise\r
-\r
-        '''\r
-        sizer.Add(self.pg, 1, wx.EXPAND)\r
-        self.SetSizer(sizer)\r
-        sizer.SetSizeHints(self)\r
-\r
-        self.SelectedTreeItem = None\r
-\r
-    def GetPropertyValues(self):\r
-        return self.pg.GetPropertyValues()\r
-\r
-    def Initialize(self, properties):\r
-        pg = self.pg\r
-        pg.Clear()\r
-\r
-        if properties:\r
-            for element in properties:\r
-                if element[1]['type'] == 'arraystring':\r
-                    elements = element[1]['elements']\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1]['value']\r
-                    else:\r
-                        property_value = element[1]['default']\r
-                    #retrieve individual strings\r
-                    property_value = split(property_value, ' ')\r
-                    #remove " delimiters\r
-                    values = [value.strip('"') for value in property_value]\r
-                    pg.Append(wxpg.ArrayStringProperty(element[0], value=values))\r
-\r
-                if element[1]['type'] == 'boolean':\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1].as_bool('value')\r
-                    else:\r
-                        property_value = element[1].as_bool('default')\r
-                    property_control = wxpg.BoolProperty(element[0], value=property_value)\r
-                    pg.Append(property_control)\r
-                    pg.SetPropertyAttribute(element[0], 'UseCheckbox', True)\r
-\r
-                #if element[0] == 'category':\r
-                    #pg.Append(wxpg.PropertyCategory(element[1]))\r
-\r
-                if element[1]['type'] == 'color':\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1]['value']\r
-                    else:\r
-                        property_value = element[1]['default']\r
-                    property_value = eval(property_value)\r
-                    pg.Append(wxpg.ColourProperty(element[0], value=property_value))\r
-\r
-                if element[1]['type'] == 'enum':\r
-                    elements = element[1]['elements']\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1]['value']\r
-                    else:\r
-                        property_value = element[1]['default']\r
-                    pg.Append(wxpg.EnumProperty(element[0], element[0], elements, [], elements.index(property_value)))\r
-\r
-                if element[1]['type'] == 'filename':\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1]['value']\r
-                    else:\r
-                        property_value = element[1]['default']\r
-                    pg.Append(wxpg.FileProperty(element[0], value=property_value))\r
-\r
-                if element[1]['type'] == 'float':\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1].as_float('value')\r
-                    else:\r
-                        property_value = element[1].as_float('default')\r
-                    property_control = wxpg.FloatProperty(element[0], value=property_value)\r
-                    pg.Append(property_control)\r
-\r
-                if element[1]['type'] == 'folder':\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1]['value']\r
-                    else:\r
-                        property_value = element[1]['default']\r
-                    pg.Append(wxpg.DirProperty(element[0], value=property_value))\r
-\r
-                if element[1]['type'] == 'integer':\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1].as_int('value')\r
-                    else:\r
-                        property_value = element[1].as_int('default')\r
-                    property_control = wxpg.IntProperty(element[0], value=property_value)\r
-                    if 'maximum' in element[1]:\r
-                        property_control.SetAttribute('Max', element[1].as_int('maximum'))\r
-                    if 'minimum' in element[1]:\r
-                        property_control.SetAttribute('Min', element[1].as_int('minimum'))\r
-                    property_control.SetAttribute('Wrap', True)\r
-                    pg.Append(property_control)\r
-                    pg.SetPropertyEditor(element[0], 'SpinCtrl')\r
-\r
-                if element[1]['type'] == 'string':\r
-                    if 'value' in element[1]:\r
-                        property_value = element[1]['value']\r
-                    else:\r
-                        property_value = element[1]['default']\r
-                    pg.Append(wxpg.StringProperty(element[0], value=property_value))\r
-\r
-        pg.Refresh()\r
-\r
-    def OnReserved(self, event):\r
-        pass\r
+        self._last_tooltip = None\r
+        self.GetGridWindow().Bind(wx.EVT_MOTION, self._on_mouse_over)\r
+\r
+    def _on_mouse_over(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
diff --git a/hooke/ui/gui/panel/propertyeditor2.py b/hooke/ui/gui/panel/propertyeditor2.py
deleted file mode 100644 (file)
index 759441e..0000000
+++ /dev/null
@@ -1,277 +0,0 @@
-# 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='propertyeditor', 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_mouse_over)\r
-\r
-    def _on_mouse_over(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