Clean up and adjust names of commands requiring curve updates in the GUI.
[hooke.git] / hooke / ui / gui / __init__.py
index 5eeff7da16ebc680343581d9dbd9af4b8d86941d..a49fe27e498b4abc37a78a225cdd7f6d7cd8a30e 100644 (file)
@@ -1,7 +1,4 @@
-# Copyright (C) 2008-2010 Fabrizio Benedetti
-#                         Massimo Sandal <devicerandom@gmail.com>
-#                         Rolf Schmidt <rschmidt@alcor.concordia.ca>
-#                         W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
 #
 # This file is part of Hooke.
 #
@@ -46,14 +43,15 @@ import wx.lib.evtmgr as evtmgr
 
 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
 from ...config import Setting
+from ...engine import CommandMessage
 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
-from ...ui import UserInterface, CommandMessage
+from ...ui import UserInterface
 from .dialog.selection import Selection as SelectionDialog
 from .dialog.save_file import select_save_file
 from . import menu as menu
 from . import navbar as navbar
 from . import panel as panel
-from .panel.propertyeditor import prop_from_argument, prop_from_setting
+from .panel.propertyeditor import props_from_argument, props_from_setting
 from . import statusbar as statusbar
 
 
@@ -70,7 +68,9 @@ class HookeFrame (wx.Frame):
         self._perspectives = {}  # {name: perspective_str}
         self._c = {}
 
-        self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
+        self.SetIcon(wx.Icon(
+                os.path.expanduser(self.gui.config['icon image']),
+                wx.BITMAP_TYPE_ICO))
 
         # setup frame manager
         self._c['manager'] = aui.AuiManager()
@@ -114,13 +114,7 @@ class HookeFrame (wx.Frame):
 
         self._setup_perspectives()
         self._bind_events()
-
-        self.execute_command(
-                command=self._command_by_name('load playlist'),
-                args={'input':'test/data/vclamp_picoforce/playlist'},
-                )
         return # TODO: cleanup
-        self.playlists = self._c['playlist'].Playlists
         self._displayed_plot = None
         #load default list, if possible
         self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
@@ -170,7 +164,7 @@ class HookeFrame (wx.Frame):
                     commands=self.commands,
                     selected=self.gui.config['selected command'],
                     callbacks={
-                        'execute': self.execute_command,
+                        'execute': self.explicit_execute_command,
                         'select_plugin': self.select_plugin,
                         'select_command': self.select_command,
 #                        'selection_changed': self.panelProperties.select(self, method, command),  #SelectedTreeItem = selected_item,
@@ -186,13 +180,9 @@ class HookeFrame (wx.Frame):
                     style=wx.WANTS_CHARS,
                     # WANTS_CHARS so the panel doesn't eat the Return key.
                     ), 'center'),
-#            ('assistant', wx.TextCtrl(
-#                    parent=self,
-#                    pos=wx.Point(0, 0),
-#                    size=wx.Size(150, 90),
-#                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
             (panel.PANELS['plot'](
                     callbacks={
+                        '_set_status_text': self._on_plot_status_text,
                         },
                     parent=self,
                     style=wx.WANTS_CHARS|wx.NO_BORDER,
@@ -205,10 +195,12 @@ class HookeFrame (wx.Frame):
                     size=wx.Size(150, 90),
                     style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
              'bottom'),
-#            ('results', panel.results.Results(self), 'bottom'),
             ]:
             self._add_panel(p, style)
-        #self._c['assistant'].SetEditable(False)
+        self.execute_command(  # setup already loaded playlists
+            command=self._command_by_name('playlists'))
+        self.execute_command(  # setup already loaded curve
+            command=self._command_by_name('get curve'))
 
     def _add_panel(self, panel, style):
         self._c[panel.name] = panel
@@ -249,17 +241,11 @@ class HookeFrame (wx.Frame):
         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
         self.Bind(wx.EVT_SIZE, self._on_size)
         self.Bind(wx.EVT_CLOSE, self._on_close)
-        self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
-        self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
+        self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
 
         return # TODO: cleanup
         treeCtrl = self._c['folders'].GetTreeCtrl()
         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
-        
-        #property editor
-        self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
-        #results panel
-        self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
 
     def _on_about(self, *args):
         dialog = wx.MessageDialog(
@@ -272,18 +258,23 @@ class HookeFrame (wx.Frame):
         dialog.ShowModal()
         dialog.Destroy()
 
+    def _on_size(self, event):
+        event.Skip()
+
     def _on_close(self, *args):
         self.log.info('closing GUI framework')
         # apply changes
-        self.gui.config['main height'] = str(self.GetSize().GetHeight())
-        self.gui.config['main left'] = str(self.GetPosition()[0])
-        self.gui.config['main top'] = str(self.GetPosition()[1])
-        self.gui.config['main width'] = str(self.GetSize().GetWidth())
-        # push changes back to Hooke.config?
+        self._set_config('main height', self.GetSize().GetHeight())
+        self._set_config('main left', self.GetPosition()[0])
+        self._set_config('main top', self.GetPosition()[1])
+        self._set_config('main width', self.GetSize().GetWidth())
         self._c['manager'].UnInit()
         del self._c['manager']
         self.Destroy()
 
+    def _on_erase_background(self, event):
+        event.Skip()
+
 
 
     # Panel utility functions
@@ -307,18 +298,57 @@ class HookeFrame (wx.Frame):
             raise Exception('Multiple commands named "%s"' % name)
         return cs[0]
 
+    def explicit_execute_command(self, _class=None, method=None,
+                                 command=None, args=None):
+        return self.execute_command(
+            _class=_class, method=method, command=command, args=args,
+            explicit_user_call=True)
+
     def execute_command(self, _class=None, method=None,
-                        command=None, args=None):
+                        command=None, args=None, explicit_user_call=False):
         if args == None:
             args = {}
         if ('property editor' in self._c
-            and self.gui.config['selected command'] == command):
-            arg_names = [arg.name for arg in command.arguments]
+            and self.gui.config['selected command'] == command.name):
             for name,value in self._c['property editor'].get_values().items():
-                if name in arg_names:
-                    args[name] = value
-        self.log.debug('executing %s with %s' % (command.name, args))
-        self.inqueue.put(CommandMessage(command, args))
+                arg = self._c['property editor']._argument_from_label.get(
+                    name, None)
+                if arg == None:
+                    continue
+                elif arg.count == 1:
+                    args[arg.name] = value
+                    continue
+                # deal with counted arguments
+                if arg.name not in args:
+                    args[arg.name] = {}
+                index = int(name[len(arg.name):])
+                args[arg.name][index] = value
+            for arg in command.arguments:
+                if arg.name not in args:
+                    continue  # undisplayed argument, e.g. 'driver' types.
+                count = arg.count
+                if hasattr(arg, '_display_count'):  # support HACK in props_from_argument()
+                    count = arg._display_count
+                if count != 1 and arg.name in args:
+                    keys = sorted(args[arg.name].keys())
+                    assert keys == range(count), keys
+                    args[arg.name] = [args[arg.name][i]
+                                      for i in range(count)]
+                if arg.count == -1:
+                    while (len(args[arg.name]) > 0
+                           and args[arg.name][-1] == None):
+                        args[arg.name].pop()
+                    if len(args[arg.name]) == 0:
+                        args[arg.name] = arg.default
+        cm = CommandMessage(command.name, args)
+        self.gui._submit_command(
+            cm, self.inqueue, explicit_user_call=explicit_user_call)
+        # TODO: skip responses for commands that were captured by the
+        # command stack.  We'd need to poll on each request, remember
+        # capture state, or add a flag to the response...
+        return self._handle_response(command_message=cm)
+
+    def _handle_response(self, command_message):
         results = []
         while True:
             msg = self.outqueue.get()
@@ -337,9 +367,11 @@ class HookeFrame (wx.Frame):
                 h.run(self, msg)  # TODO: pause for response?
                 continue
         pp = getattr(
-            self, '_postprocess_%s' % command.name.replace(' ', '_'),
-            self._postprocess_text)
-        pp(command=command, args=args, results=results)
+           self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
+           self._postprocess_text)
+        pp(command=command_message.command,
+           args=command_message.arguments,
+           results=results)
         return results
 
     def _handle_request(self, msg):
@@ -365,6 +397,10 @@ class HookeFrame (wx.Frame):
                 continue
         self.inqueue.put(response)
 
+    def _set_config(self, option, value, section=None):
+        self.gui._set_config(section=section, option=option, value=value,
+                             ui_to_command_queue=self.inqueue,
+                             response_handler=self._handle_response)
 
 
     # Command-specific postprocessing
@@ -382,6 +418,34 @@ class HookeFrame (wx.Frame):
                 self._c['output'].write(result.__class__.__name__+'\n')
             self._c['output'].write(str(result).rstrip()+'\n')
 
+    def _postprocess_playlists(self, command, args={}, results=None):
+        """Update `self` to show the playlists.
+        """
+        if not isinstance(results[-1], Success):
+            self._postprocess_text(command, results=results)
+            return
+        assert len(results) == 2, results
+        playlists = results[0]
+        if 'playlist' in self._c:
+            for playlist in playlists:
+                if self._c['playlist'].is_playlist_loaded(playlist):
+                    self._c['playlist'].update_playlist(playlist)
+                else:
+                    self._c['playlist'].add_playlist(playlist)
+
+    def _postprocess_new_playlist(self, command, args={}, results=None):
+        """Update `self` to show the new playlist.
+        """
+        if not isinstance(results[-1], Success):
+            self._postprocess_text(command, results=results)
+            return
+        assert len(results) == 2, results
+        playlist = results[0]
+        if 'playlist' in self._c:
+            loaded = self._c['playlist'].is_playlist_loaded(playlist)
+            assert loaded == False, loaded
+            self._c['playlist'].add_playlist(playlist)
+
     def _postprocess_load_playlist(self, command, args={}, results=None):
         """Update `self` to show the playlist.
         """
@@ -390,7 +454,7 @@ class HookeFrame (wx.Frame):
             return
         assert len(results) == 2, results
         playlist = results[0]
-        self._c['playlist']._c['tree'].add_playlist(playlist)
+        self._c['playlist'].add_playlist(playlist)
 
     def _postprocess_get_playlist(self, command, args={}, results=[]):
         if not isinstance(results[-1], Success):
@@ -398,7 +462,10 @@ class HookeFrame (wx.Frame):
             return
         assert len(results) == 2, results
         playlist = results[0]
-        self._c['playlist']._c['tree'].update_playlist(playlist)
+        if 'playlist' in self._c:
+            loaded = self._c['playlist'].is_playlist_loaded(playlist)
+            assert loaded == True, loaded
+            self._c['playlist'].update_playlist(playlist)
 
     def _postprocess_get_curve(self, command, args={}, results=[]):
         """Update `self` to show the curve.
@@ -416,9 +483,9 @@ class HookeFrame (wx.Frame):
         else:
             raise NotImplementedError()
         if 'note' in self._c:
-            self._c['note'].set_text(curve.info['note'])
+            self._c['note'].set_text(curve.info.get('note', ''))
         if 'playlist' in self._c:
-            self._c['playlist']._c['tree'].set_selected_curve(
+            self._c['playlist'].set_selected_curve(
                 playlist, curve)
         if 'plot' in self._c:
             self._c['plot'].set_curve(curve, config=self.gui.config)
@@ -433,16 +500,26 @@ class HookeFrame (wx.Frame):
         """
         pass
 
-    def _postprocess_zero_block_surface_contact_point(
+    def _postprocess_glob_curves_to_playlist(
         self, command, args={}, results=[]):
-        """Update the curve, since the available columns may have changed.
+        """Update `self` to show new curves.
         """
-        if isinstance(results[-1], Success):
-            self.execute_command(
-                command=self._command_by_name('get curve'))
-    def _postprocess_add_block_force_array(
-        self, command, args={}, results=[]):
+        if not isinstance(results[-1], Success):
+            self._postprocess_text(command, results=results)
+            return
+        if 'playlist' in self._c:
+            if args.get('playlist', None) != None:
+                playlist = args['playlist']
+                pname = playlist.name
+                loaded = self._c['playlist'].is_playlist_name_loaded(pname)
+                assert loaded == True, loaded
+                for curve in results[:-1]:
+                    self._c['playlist']._add_curve(pname, curve)
+            else:
+                self.execute_command(
+                    command=self._command_by_name('get playlist'))
+
+    def _update_curve(self, command, args={}, results=[]):
         """Update the curve, since the available columns may have changed.
         """
         if isinstance(results[-1], Success):
@@ -450,209 +527,12 @@ class HookeFrame (wx.Frame):
                 command=self._command_by_name('get curve'))
 
 
-
-    # TODO: cruft
-
-    def _GetActiveFileIndex(self):
-        lib.playlist.Playlist = self.GetActivePlaylist()
-        #get the selected item from the tree
-        selected_item = self._c['playlist']._c['tree'].GetSelection()
-        #test if a playlist or a curve was double-clicked
-        if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
-            return -1
-        else:
-            count = 0
-            selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
-            while selected_item.IsOk():
-                count += 1
-                selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
-            return count
-
-    def _GetPlaylistTab(self, name):
-        for index, page in enumerate(self._c['notebook']._tabs._pages):
-            if page.caption == name:
-                return index
-        return -1
-
-    def select_plugin(self, _class=None, method=None, plugin=None):
-        pass
-
-    def AddPlaylistFromFiles(self, files=[], name='Untitled'):
-        if files:
-            playlist = lib.playlist.Playlist(self, self.drivers)
-            for item in files:
-                playlist.add_curve(item)
-        if playlist.count > 0:
-            playlist.name = self._GetUniquePlaylistName(name)
-            playlist.reset()
-            self.AddTayliss(playlist)
-
-    def AppliesPlotmanipulator(self, name):
-        '''
-        Returns True if the plotmanipulator 'name' is applied, False otherwise
-        name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
-        '''
-        return self.GetBoolFromConfig('core', 'plotmanipulators', name)
-
-    def ApplyPlotmanipulators(self, plot, plot_file):
-        '''
-        Apply all active plotmanipulators.
-        '''
-        if plot is not None and plot_file is not None:
-            manipulated_plot = copy.deepcopy(plot)
-            for plotmanipulator in self.plotmanipulators:
-                if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
-                    manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
-            return manipulated_plot
-
-    def GetActiveFigure(self):
-        playlist_name = self.GetActivePlaylistName()
-        figure = self.playlists[playlist_name].figure
-        if figure is not None:
-            return figure
-        return None
-
-    def GetActiveFile(self):
-        playlist = self.GetActivePlaylist()
-        if playlist is not None:
-            return playlist.get_active_file()
-        return None
-
-    def GetActivePlot(self):
-        playlist = self.GetActivePlaylist()
-        if playlist is not None:
-            return playlist.get_active_file().plot
-        return None
-
-    def GetDisplayedPlot(self):
-        plot = copy.deepcopy(self.displayed_plot)
-        #plot.curves = []
-        #plot.curves = copy.deepcopy(plot.curves)
-        return plot
-
-    def GetDisplayedPlotCorrected(self):
-        plot = copy.deepcopy(self.displayed_plot)
-        plot.curves = []
-        plot.curves = copy.deepcopy(plot.corrected_curves)
-        return plot
-
-    def GetDisplayedPlotRaw(self):
-        plot = copy.deepcopy(self.displayed_plot)
-        plot.curves = []
-        plot.curves = copy.deepcopy(plot.raw_curves)
-        return plot
-
-    def GetDockArt(self):
-        return self._c['manager'].GetArtProvider()
-
-    def GetPlotmanipulator(self, name):
-        '''
-        Returns a plot manipulator function from its name
-        '''
-        for plotmanipulator in self.plotmanipulators:
-            if plotmanipulator.name == name:
-                return plotmanipulator
-        return None
-
-    def HasPlotmanipulator(self, name):
-        '''
-        returns True if the plotmanipulator 'name' is loaded, False otherwise
-        '''
-        for plotmanipulator in self.plotmanipulators:
-            if plotmanipulator.command == name:
-                return True
-        return False
-
-
-    def _on_dir_ctrl_left_double_click(self, event):
-        file_path = self.panelFolders.GetPath()
-        if os.path.isfile(file_path):
-            if file_path.endswith('.hkp'):
-                self.do_loadlist(file_path)
-        event.Skip()
-
-    def _on_erase_background(self, event):
-        event.Skip()
-
-    def _on_notebook_page_close(self, event):
-        ctrl = event.GetEventObject()
-        playlist_name = ctrl.GetPageText(ctrl._curpage)
-        self.DeleteFromPlaylists(playlist_name)
-
-    def OnPaneClose(self, event):
-        event.Skip()
-
-    def OnPropGridChanged (self, event):
-        prop = event.GetProperty()
-        if prop:
-            item_section = self.panelProperties.SelectedTreeItem
-            item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
-            plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
-            config = self.gui.config[plugin]
-            property_section = self._c['commands']._c['tree'].GetItemText(item_section)
-            property_key = prop.GetName()
-            property_value = prop.GetDisplayedString()
-
-            config[property_section][property_key]['value'] = property_value
-
-    def OnResultsCheck(self, index, flag):
-        results = self.GetActivePlot().results
-        if results.has_key(self.results_str):
-            results[self.results_str].results[index].visible = flag
-            results[self.results_str].update()
-            self.UpdatePlot()
-
-
-    def _on_size(self, event):
-        event.Skip()
-
-    def UpdatePlaylistsTreeSelection(self):
-        playlist = self.GetActivePlaylist()
-        if playlist is not None:
-            if playlist.index >= 0:
-                self._c['status bar'].set_playlist(playlist)
-                self.UpdateNote()
-                self.UpdatePlot()
-
-    def _on_curve_select(self, playlist, curve):
-        #create the plot tab and add playlist to the dictionary
-        plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
-        notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
-        #tab_index = self._c['notebook'].GetSelection()
-        playlist.figure = plotPanel.get_figure()
-        self.playlists[playlist.name] = playlist
-        #self.playlists[playlist.name] = [playlist, figure]
-        self._c['status bar'].set_playlist(playlist)
-        self.UpdateNote()
-        self.UpdatePlot()
-
-
-    def _on_playlist_left_doubleclick(self):
-        index = self._c['notebook'].GetSelection()
-        current_playlist = self._c['notebook'].GetPageText(index)
-        if current_playlist != playlist_name:
-            index = self._GetPlaylistTab(playlist_name)
-            self._c['notebook'].SetSelection(index)
-        self._c['status bar'].set_playlist(playlist)
-        self.UpdateNote()
-        self.UpdatePlot()
-
-    def _on_playlist_delete(self, playlist):
-        notebook = self.Parent.plotNotebook
-        index = self.Parent._GetPlaylistTab(playlist.name)
-        notebook.SetSelection(index)
-        notebook.DeletePage(notebook.GetSelection())
-        self.Parent.DeleteFromPlaylists(playlist_name)
-
-
-
     # Command panel interface
 
     def select_command(self, _class, method, command):
         #self.select_plugin(plugin=command.plugin)
-        if 'assistant' in self._c:
-            self._c['assitant'].ChangeValue(command.help)
         self._c['property editor'].clear()
+        self._c['property editor']._argument_from_label = {}
         for argument in command.arguments:
             if argument.name == 'help':
                 continue
@@ -673,13 +553,30 @@ class HookeFrame (wx.Frame):
             else:
                 curves = results[0]
 
-            p = prop_from_argument(
+            ret = props_from_argument(
                 argument, curves=curves, playlists=playlists)
-            if p == None:
+            if ret == None:
                 continue  # property intentionally not handled (yet)
-            self._c['property editor'].append_property(p)
+            for label,p in ret:
+                self._c['property editor'].append_property(p)
+                self._c['property editor']._argument_from_label[label] = (
+                    argument)
+
+        self._set_config('selected command', command.name)
+
+    def select_plugin(self, _class=None, method=None, plugin=None):
+        pass
+
 
-        self.gui.config['selected command'] = command  # TODO: push to engine
+
+    # Folders panel interface
+
+    def _on_dir_ctrl_left_double_click(self, event):
+        file_path = self.panelFolders.GetPath()
+        if os.path.isfile(file_path):
+            if file_path.endswith('.hkp'):
+                self.do_loadlist(file_path)
+        event.Skip()
 
 
 
@@ -707,6 +604,7 @@ class HookeFrame (wx.Frame):
         pass
 
     def _on_delete_curve(self, _class, method, playlist, curve):
+        # TODO: execute_command 'remove curve from playlist'
         os.remove(curve.path)
 
     def _on_set_selected_playlist(self, _class, method, playlist):
@@ -744,6 +642,14 @@ class HookeFrame (wx.Frame):
 
 
 
+    # Plot panel interface
+
+    def _on_plot_status_text(self, _class, method, text):
+        if 'status bar' in self._c:
+            self._c['status bar'].set_plot_text(text)
+
+
+
     # Navbar interface
 
     def _next_curve(self, *args):
@@ -768,6 +674,13 @@ class HookeFrame (wx.Frame):
 
     # Panel display handling
 
+    def _on_pane_close(self, event):
+        pane = event.pane
+        view = self._c['menu bar']._c['view']
+        if pane.name in  view._c.keys():
+            view._c[pane.name].Check(False)
+        event.Skip()
+
     def _on_panel_visibility(self, _class, method, panel_name, visible):
         pane = self._c['manager'].GetPane(panel_name)
         pane.Show(visible)
@@ -783,7 +696,7 @@ class HookeFrame (wx.Frame):
         self._perspectives = {
             'Default': self._c['manager'].SavePerspective(),
             }
-        path = self.gui.config['perspective path']
+        path = os.path.expanduser(self.gui.config['perspective path'])
         if os.path.isdir(path):
             files = sorted(os.listdir(path))
             for fname in files:
@@ -801,7 +714,7 @@ class HookeFrame (wx.Frame):
 
         selected_perspective = self.gui.config['active perspective']
         if not self._perspectives.has_key(selected_perspective):
-            self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke
+            self._set_config('active perspective', 'Default')
 
         self._restore_perspective(selected_perspective, force=True)
         self._update_perspective_menu()
@@ -846,7 +759,7 @@ class HookeFrame (wx.Frame):
     def _restore_perspective(self, name, force=False):
         if name != self.gui.config['active perspective'] or force == True:
             self.log.debug('restore perspective %s' % name)
-            self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke
+            self._set_config('active perspective', name)
             self._c['manager'].LoadPerspective(self._perspectives[name])
             self._c['manager'].Update()
             for pane in self._c['manager'].GetAllPanes():
@@ -860,7 +773,7 @@ class HookeFrame (wx.Frame):
         if name == 'Default':
             name = 'New perspective'
         name = select_save_file(
-            directory=self.gui.config['perspective path'],
+            directory=os.path.expanduser(self.gui.config['perspective path']),
             name=name,
             extension=self.gui.config['perspective extension'],
             parent=self,
@@ -869,7 +782,8 @@ class HookeFrame (wx.Frame):
         if name == None:
             return
         self._save_perspective(
-            perspective, self.gui.config['perspective path'], name=name,
+            perspective,
+            os.path.expanduser(self.gui.config['perspective path']), name=name,
             extension=self.gui.config['perspective extension'])
 
     def _on_delete_perspective(self, *args, **kwargs):
@@ -885,16 +799,27 @@ class HookeFrame (wx.Frame):
             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
         dialog.CenterOnScreen()
         dialog.ShowModal()
+        if dialog.canceled == True:
+            return
         names = [options[i] for i in dialog.selected]
         dialog.Destroy()
         self._delete_perspectives(
-            self.gui.config['perspective path'], names=names,
-            extension=self.gui.config['perspective extension'])
+            os.path.expanduser(self.gui.config['perspective path']),
+            names=names, extension=self.gui.config['perspective extension'])
 
     def _on_select_perspective(self, _class, method, name):
         self._restore_perspective(name)
 
 
+# setup per-command versions of HookeFrame._update_curve
+for _command in ['convert_distance_to_force',
+                 'polymer_fit_peaks',
+                 'remove_cantilever_from_extension',
+                 'zero_surface_contact_point',
+                 ]:
+    setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
+del _command
+
 
 class HookeApp (wx.App):
     """A :class:`wx.App` wrapper around :class:`HookeFrame`.
@@ -941,7 +866,7 @@ class HookeApp (wx.App):
 
     def _setup_splash_screen(self):
         if self.gui.config['show splash screen'] == True:
-            path = self.gui.config['splash screen image']
+            path = os.path.expanduser(self.gui.config['splash screen image'])
             if os.path.isfile(path):
                 duration = self.gui.config['splash screen duration']
                 wx.SplashScreen(