hooke.ui.gui was getting complicated, so I stripped it down for a moment.
authorW. Trevor King <wking@drexel.edu>
Tue, 27 Jul 2010 14:30:23 +0000 (10:30 -0400)
committerW. Trevor King <wking@drexel.edu>
Tue, 27 Jul 2010 14:30:23 +0000 (10:30 -0400)
I want HookeFrame to be a callback clearinghouse.  Flow will look like

     panels/menus/navbars
        |       ^
  callbacks   methods
        v       |v--(response processors)-,
       Hooke Frame                       engine
                `---(execute_command)----^

With the following naming scheme in HookeFrame:
  callbacks:           _on_*
  response processors: _postprocess_*

Also:
* more use of hooke.util.pluggable for handling extendible submods.

23 files changed:
doc/hacking.txt
hooke/driver/__init__.py
hooke/plugin/curve.py
hooke/ui/__init__.py
hooke/ui/gui/__init__.py
hooke/ui/gui/dialog/__init__.py [new file with mode: 0644]
hooke/ui/gui/dialog/selection.py [moved from hooke/ui/gui/panel/selection.py with 57% similarity]
hooke/ui/gui/dialog/string.py [new file with mode: 0644]
hooke/ui/gui/handler/__init__.py [new file with mode: 0644]
hooke/ui/gui/handler/boolean.py [new file with mode: 0644]
hooke/ui/gui/handler/float.py [new file with mode: 0644]
hooke/ui/gui/handler/selection.py [new file with mode: 0644]
hooke/ui/gui/handler/string.py [new file with mode: 0644]
hooke/ui/gui/menu.py
hooke/ui/gui/navbar.py
hooke/ui/gui/panel/__init__.py
hooke/ui/gui/panel/commands.py
hooke/ui/gui/panel/note.py
hooke/ui/gui/panel/notebook.py
hooke/ui/gui/panel/playlist.py
hooke/ui/gui/panel/welcome.py
hooke/ui/gui/statusbar.py
hooke/util/pluggable.py

index fb9a7d444ff571d4f5e49b145d54d62937e51b33..70f2f8b62137ea8b97d35507ff7641768d1e3544 100644 (file)
@@ -36,6 +36,21 @@ the :doc:`testing` section for more information.
 .. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.3/
 
 
+Principles
+==========
+
+Hooke aims to be easily modified and extended by new recruits.  To
+make this easier, we try to abide by several programming practices.
+
+* `DRY`_ (Don't Repeat Yourself), also phrased as "Every piece of
+  knowledge must have a single, unambiguous, authoritative
+  representation within a system."
+* `LoD`_ (Law of Demeter): Don't reach through layers, e.g. `a.b.c.d()`.
+
+.. _DRY: http://en.wikipedia.org/wiki/Don%27t_repeat_yourself
+.. _LoD: http://en.wikipedia.org/wiki/Law_of_Demeter
+
+
 Architecture
 ============
 
index cdfb4aecbecbbb7e8cd14215a532f56581cf3d10..e51fb225e9bc5add71a229b143e4d3ab26cf7973 100644 (file)
@@ -51,10 +51,10 @@ DRIVER_SETTING_SECTION = 'drivers'
 """
 
 
-class Driver(object):
+class Driver (object):
     """Base class for file format drivers.
     
-    :attr:`name` identifies your driver, and should match the module
+    :attr:`name` identifies your driver and should match the module
     name.
     """
     def __init__(self, name):
index 9471318f81582f2055f259a145ffd9b66121104a..56b43799ffd358096b6ddd142853bf38acc42771 100644 (file)
@@ -38,7 +38,7 @@ class CurvePlugin (Builtin):
     def __init__(self):
         super(CurvePlugin, self).__init__(name='curve')
         self._commands = [
-            InfoCommand(self), ExportCommand(self),
+            GetCommand(self), InfoCommand(self), ExportCommand(self),
             DifferenceCommand(self), DerivativeCommand(self),
             PowerSpectrumCommand(self)]
 
@@ -64,6 +64,17 @@ of the current playlist.
 
 # Define commands
 
+class GetCommand (Command):
+    """Return a :class:`hooke.curve.Curve`.
+    """
+    def __init__(self, plugin):
+        super(GetCommand, self).__init__(
+            name='get curve', arguments=[CurveArgument],
+            help=self.__doc__, plugin=plugin)
+
+    def _run(self, hooke, inqueue, outqueue, params):
+        outqueue.put(params['curve'])
+
 class InfoCommand (Command):
     """Get selected information about a :class:`hooke.curve.Curve`.
     """
index 42085c25a0b05b5216cb8263004ae4158a4e1b54..47f97351d2f747bbaca71c30470e0e3fe64d5ed4 100644 (file)
@@ -22,9 +22,8 @@
 import ConfigParser as configparser
 
 from .. import version
-from ..compat.odict import odict
 from ..config import Setting
-from ..util.pluggable import IsSubclass
+from ..util.pluggable import IsSubclass, construct_odict
 
 
 USER_INTERFACE_MODULES = [
@@ -52,8 +51,10 @@ class CommandMessage (QueueMessage):
     a :class:`dict` with `argname` keys and `value` values to be
     passed to the command.
     """
-    def __init__(self, command, arguments):
+    def __init__(self, command, arguments=None):
         self.command = command
+        if arguments == None:
+            arguments = {}
         self.arguments = arguments
 
 class UserInterface (object):
@@ -106,27 +107,6 @@ COPYRIGHT
         return 'The playlist %s does not contain any valid force curve data.' \
             % self.name
 
-def construct_odict(this_modname, submodnames, class_selector):
-    """Search the submodules `submodnames` of a module `this_modname`
-    for class objects for which `class_selector(class)` returns
-    `True`.  These classes are instantiated and stored in the returned
-    :class:`hooke.compat.odict.odict` in the order in which they were
-    discovered.
-    """
-    instances = odict()
-    for submodname in submodnames:
-        count = len([s for s in submodnames if s == submodname])
-        assert count > 0, 'No %s entries: %s' % (submodname, submodnames)
-        assert count == 1, 'Multiple (%d) %s entries: %s' \
-            % (count, submodname, submodnames)
-        this_mod = __import__(this_modname, fromlist=[submodname])
-        submod = getattr(this_mod, submodname)
-        for objname in dir(submod):
-            obj = getattr(submod, objname)
-            if class_selector(obj):
-                instance = obj()
-                instances[instance.name] = instance
-    return instances
 
 USER_INTERFACES = construct_odict(
     this_modname=__name__,
index 69f5a61588a7c698ba9bdbbb45f3a43942b722ae..f83936d0a1c09959f042a6ccecbca58e5af4c95d 100644 (file)
@@ -1,6 +1,6 @@
 # Copyright\r
 \r
-"""Defines :class:`GUI` providing a wxWindows interface to Hooke.\r
+"""Defines :class:`GUI` providing a wxWidgets interface to Hooke.\r
 """\r
 \r
 WX_GOOD=['2.8']\r
@@ -26,8 +26,7 @@ import wx.lib.evtmgr as evtmgr
 \r
 from matplotlib.ticker import FuncFormatter\r
 \r
-from ... import version\r
-from ...command import CommandExit, Exit, Command, Argument, StoreValue\r
+from ...command import CommandExit, Exit, Success, Failure, Command, Argument\r
 from ...config import Setting\r
 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig\r
 from ...ui import UserInterface, CommandMessage\r
@@ -75,14 +74,18 @@ class HookeFrame (wx.Frame):
 \r
         # Create the menubar after the panes so that the default\r
         # perspective is created with all panes open\r
-        self._c['menu bar'] = menu.MenuBar(\r
-            callbacks={},\r
-            )\r
+        self._c['menu bar'] = menu.HookeMenuBar(\r
+            parent=self,\r
+            callbacks={\r
+                'close': self._on_close,\r
+                'about': self._on_about,\r
+                })\r
         self.SetMenuBar(self._c['menu bar'])\r
 \r
-        self._c['status bar'] = statubar.StatusBar(\r
+        self._c['status bar'] = statusbar.StatusBar(\r
             parent=self,\r
             style=wx.ST_SIZEGRIP)\r
+        self.SetStatusBar(self._c['status bar'])\r
 \r
         self._update_perspectives()\r
         self._bind_events()\r
@@ -101,28 +104,31 @@ class HookeFrame (wx.Frame):
     def _setup_panels(self):\r
         client_size = self.GetClientSize()\r
         for label,p,style in [\r
-            ('folders', wx.GenericDirCtrl(\r
-                    parent=self,\r
-                    dir=self.gui.config['folders-workdir'],\r
-                    size=(200, 250),\r
-                    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.playlist.Playlist(\r
-                    config=self.gui.config,\r
-                    callbacks={},\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)), 'left'),\r
-            ('note', panel.note.Note(self), 'left'),\r
-            ('notebook', Notebook(\r
-                    parent=self,\r
-                    pos=wx.Point(client_size.x, client_size.y),\r
-                    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.commands.Commands(\r
+#            ('folders', wx.GenericDirCtrl(\r
+#                    parent=self,\r
+#                    dir=self.gui.config['folders-workdir'],\r
+#                    size=(200, 250),\r
+#                    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
+#                    callbacks={},\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
+#                    size=(160, 200)), 'left'),\r
+#            ('note', panel.note.Note(\r
+#                    parent=self\r
+#                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
+#                    size=(160, 200)), 'left'),\r
+#            ('notebook', Notebook(\r
+#                    parent=self,\r
+#                    pos=wx.Point(client_size.x, client_size.y),\r
+#                    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
                     commands=self.commands,\r
                     selected=self.gui.config['selected command'],\r
                     callbacks={\r
@@ -134,49 +140,52 @@ class HookeFrame (wx.Frame):
                     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)), 'right'),\r
+#                    size=(160, 200)\r
+                    ), 'center'),\r
             #('properties', panel.propertyeditor.PropertyEditor(self),'right'),\r
-            ('assistant', wx.TextCtrl(\r
-                    parent=self,\r
-                    pos=wx.Point(0, 0),\r
-                    size=wx.Size(150, 90),\r
-                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),\r
-            ('output', wx.TextCtrl(\r
-                    parent=self,\r
-                    pos=wx.Point(0, 0),\r
-                    size=wx.Size(150, 90),\r
-                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),\r
-            ('results', panel.results.Results(self), 'bottom'),\r
+#            ('assistant', wx.TextCtrl(\r
+#                    parent=self,\r
+#                    pos=wx.Point(0, 0),\r
+#                    size=wx.Size(150, 90),\r
+#                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),\r
+#            ('output', wx.TextCtrl(\r
+#                    parent=self,\r
+#                    pos=wx.Point(0, 0),\r
+#                    size=wx.Size(150, 90),\r
+#                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),\r
+#            ('results', panel.results.Results(self), 'bottom'),\r
             ]:\r
             self._add_panel(label, p, style)\r
-        self._c['assistant'].SetEditable(False)\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
-        if style == 'left':\r
-            info.Left().CloseButton(True).MaximizeButton(False)\r
+        info.PaneBorder(False).CloseButton(True).MaximizeButton(False)\r
+        if style == 'top':\r
+            info.Top()\r
         elif style == 'center':\r
-            info.CenterPane().PaneBorder(False)\r
+            info.CenterPane()\r
+        elif style == 'left':\r
+            info.Left()\r
         elif style == 'right':\r
-            info.Right().CloseButton(True).MaximizeButton(False)\r
+            info.Right()\r
         else:\r
             assert style == 'bottom', style\r
-            info.Bottom().CloseButton(True).MaximizeButton(False)\r
+            info.Bottom()\r
         self._c['manager'].AddPane(panel, info)\r
 \r
     def _setup_toolbars(self):\r
-        self._c['navbar'] = navbar.NavBar(\r
+        self._c['navigation bar'] = navbar.NavBar(\r
             callbacks={\r
                 'next': self._next_curve,\r
                 'previous': self._previous_curve,\r
                 },\r
             parent=self,\r
             style=wx.TB_FLAT | wx.TB_NODIVIDER)\r
-\r
         self._c['manager'].AddPane(\r
-            self._c['navbar'],\r
+            self._c['navigation bar'],\r
             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'\r
                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False\r
                 ).RightDockable(False))\r
@@ -188,11 +197,10 @@ class HookeFrame (wx.Frame):
         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)\r
         self.Bind(wx.EVT_SIZE, self._on_size)\r
         self.Bind(wx.EVT_CLOSE, self._on_close)\r
-        self.Bind(wx.EVT_MENU, self._on_close, id=wx.ID_EXIT)\r
-        self.Bind(wx.EVT_MENU, self._on_about, id=wx.ID_ABOUT)\r
         self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)\r
         self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)\r
 \r
+        return # TODO: cleanup\r
         for value in self._c['menu bar']._c['view']._c.values():\r
             self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)\r
 \r
@@ -212,6 +220,65 @@ class HookeFrame (wx.Frame):
         #results panel\r
         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck\r
 \r
+    def _command_by_name(self, name):\r
+        cs = [c for c in self.commands if c.name == name]\r
+        if len(cs) == 0:\r
+            raise KeyError(name)\r
+        elif len(cs) > 1:\r
+            raise Exception('Multiple commands named "%s"' % name)\r
+        return cs[0]\r
+\r
+    def execute_command(self, _class=None, method=None,\r
+                        command=None, args=None):\r
+        self.inqueue.put(CommandMessage(command, args))\r
+        results = []\r
+        while True:\r
+            msg = self.outqueue.get()\r
+            results.append(msg)\r
+            print type(msg), msg\r
+            if isinstance(msg, Exit):\r
+                self._on_close()\r
+                break\r
+            elif isinstance(msg, CommandExit):\r
+                # TODO: display command complete\r
+                break\r
+            elif isinstance(msg, ReloadUserInterfaceConfig):\r
+                self.gui.reload_config(msg.config)\r
+                continue\r
+            elif isinstance(msg, Request):\r
+                h = handler.HANDLERS[msg.type]\r
+                h.run(self, msg)  # TODO: pause for response?\r
+                continue\r
+        pp = getattr(\r
+            self, '_postprocess_%s' % command.name.replace(' ', '_'), None)\r
+        if pp != None:\r
+            pp(command=command, results=results)\r
+        return results\r
+\r
+    def _handle_request(self, msg):\r
+        """Repeatedly try to get a response to `msg`.\r
+        """\r
+        if prompt == None:\r
+            raise NotImplementedError('_%s_request_prompt' % msg.type)\r
+        prompt_string = prompt(msg)\r
+        parser = getattr(self, '_%s_request_parser' % msg.type, None)\r
+        if parser == None:\r
+            raise NotImplementedError('_%s_request_parser' % msg.type)\r
+        error = None\r
+        while True:\r
+            if error != None:\r
+                self.cmd.stdout.write(''.join([\r
+                        error.__class__.__name__, ': ', str(error), '\n']))\r
+            self.cmd.stdout.write(prompt_string)\r
+            value = parser(msg, self.cmd.stdin.readline())\r
+            try:\r
+                response = msg.response(value)\r
+                break\r
+            except ValueError, error:\r
+                continue\r
+        self.inqueue.put(response)\r
+\r
+\r
     def _GetActiveFileIndex(self):\r
         lib.playlist.Playlist = self.GetActivePlaylist()\r
         #get the selected item from the tree\r
@@ -249,34 +316,12 @@ class HookeFrame (wx.Frame):
         perspectivesFile.write(perspective)\r
         perspectivesFile.close()\r
 \r
-    def execute_command(self, _class, method, command, args):\r
-        self.cmd.inqueue.put(CommandMessage(command, args))\r
-        while True:\r
-            msg = self.cmd.outqueue.get()\r
-            if isinstance(msg, Exit):\r
-                return True\r
-            elif isinstance(msg, CommandExit):\r
-                self.cmd.stdout.write(msg.__class__.__name__+'\n')\r
-                self.cmd.stdout.write(str(msg).rstrip()+'\n')\r
-                break\r
-            elif isinstance(msg, ReloadUserInterfaceConfig):\r
-                self.cmd.ui.reload_config(msg.config)\r
-                continue\r
-            elif isinstance(msg, Request):\r
-                self._handle_request(msg)\r
-                continue\r
-            self.cmd.stdout.write(str(msg).rstrip()+'\n')\r
-                #TODO: run the command\r
-                #command = ''.join(['self.do_', item_text, '()'])\r
-                #self.AppendToOutput(command + '\n')\r
-                #exec(command)\r
-\r
-    def select_plugin(self, _class, method, plugin):\r
+    def select_plugin(self, _class=None, method=None, plugin=None):\r
         for option in config[section]:\r
             properties.append([option, config[section][option]])\r
 \r
     def select_command(self, _class, method, command):\r
-        self.select_plugin(command.plugin)\r
+        self.select_plugin(plugin=command.plugin)\r
         plugin = self.GetItemText(selected_item)\r
         if plugin != 'core':\r
             doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')\r
@@ -375,7 +420,7 @@ class HookeFrame (wx.Frame):
             perspectives_list.sort()\r
             index = perspectives_list.index(name)\r
             perspective_Id = ID_FirstPerspective + index\r
-            menu_item = self.MenuBar.FindItemById(perspective_Id)\r
+            menu_item = self._c['menu bar'].FindItemById(perspective_Id)\r
             return menu_item\r
         else:\r
             return None\r
@@ -389,17 +434,16 @@ class HookeFrame (wx.Frame):
                 return True\r
         return False\r
 \r
-    def _on_about(self, event):\r
-        message = 'Hooke\n\n'+\\r
-            'A free, open source data analysis platform\n\n'+\\r
-            'Copyright 2006-2008 by Massimo Sandal\n'+\\r
-            'Copyright 2010 by Dr. Rolf Schmidt\n\n'+\\r
-            'Hooke is released under the GNU General Public License version 2.'\r
-        dialog = wx.MessageDialog(self, message, 'About Hooke', wx.OK | wx.ICON_INFORMATION)\r
+    def _on_about(self, *args):\r
+        dialog = wx.MessageDialog(\r
+            parent=self,\r
+            message=self.gui._splash_text(),\r
+            caption='About Hooke',\r
+            style=wx.OK|wx.ICON_INFORMATION)\r
         dialog.ShowModal()\r
         dialog.Destroy()\r
 \r
-    def _on_close(self, event):\r
+    def _on_close(self, *args):\r
         # apply changes\r
         self.gui.config['main height'] = str(self.GetSize().GetHeight())\r
         self.gui.config['main left'] = str(self.GetPosition()[0])\r
@@ -446,13 +490,13 @@ class HookeFrame (wx.Frame):
             self._on_restore_perspective)\r
 \r
     def _on_restore_perspective(self, event):\r
-        name = self.MenuBar.FindItemById(event.GetId()).GetLabel()\r
+        name = self._c['menu bar'].FindItemById(event.GetId()).GetLabel()\r
         self._restore_perspective(name)\r
 \r
     def _on_save_perspective(self, event):\r
         def nameExists(name):\r
-            menu_position = self.MenuBar.FindMenu('Perspective')\r
-            menu = self.MenuBar.GetMenu(menu_position)\r
+            menu_position = self._c['menu bar'].FindMenu('Perspective')\r
+            menu = self._c['menu bar'].GetMenu(menu_position)\r
             for item in menu.GetMenuItems():\r
                 if item.GetText() == name:\r
                     return True\r
@@ -512,7 +556,8 @@ class HookeFrame (wx.Frame):
             options=sorted(os.listdir(self.gui.config['perspective path'])),\r
             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",\r
             button_id=wx.ID_DELETE,\r
-            button_callback=self._on_delete_perspective,\r
+            callbacks={'button': self._on_delete_perspective},\r
+            selection_style='multiple',\r
             parent=self,\r
             label='Delete perspective(s)',\r
             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
@@ -525,13 +570,13 @@ 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 _on_delete_perspective(self, event, items, selected_items):\r
-        for item in selected_items:\r
-            self._perspectives.remove(item)\r
-            if item == self.gui.config['active perspective']:\r
+    def _on_delete_perspective(self, _class, method, options, selected):\r
+        for p in selected:\r
+            self._perspectives.remove(p)\r
+            if p == self.gui.config['active perspective']:\r
                 self.gui.config['active perspective'] = 'Default'\r
             path = os.path.join(self.gui.config['perspective path'],\r
-                                item+'.txt')\r
+                                p+'.txt')\r
             remove(path)\r
         self._update_perspective_menu()\r
 \r
@@ -546,13 +591,32 @@ class HookeFrame (wx.Frame):
         event.Skip()\r
 \r
     def _next_curve(self, *args):\r
-        '''\r
-        NEXT\r
-        Go to the next curve in the playlist.\r
-        If we are at the last curve, we come back to the first.\r
-        -----\r
-        Syntax: next, n\r
-        '''\r
+        """Call the `next curve` command.\r
+        """\r
+        results = self.execute_command(\r
+            command=self._command_by_name('next curve'))\r
+        if isinstance(results[-1], Success):\r
+            self.execute_command(\r
+                command=self._command_by_name('get curve'))\r
+\r
+    def _previous_curve(self, *args):\r
+        """Call the `previous curve` command.\r
+        """\r
+        self.execute_command(\r
+            command=self._command_by_name('previous curve'))\r
+        if isinstance(results[-1], Success):\r
+            self.execute_command(\r
+                command=self._command_by_name('get curve'))\r
+\r
+    def _postprocess_get_curve(self, command, results):\r
+        """Update `self` to show the curve.\r
+        """\r
+        if not isinstance(results[-1], Success):\r
+            return  # error executing 'get curve'\r
+        assert len(results) == 2, results\r
+        curve = results[0]\r
+        print curve\r
+\r
         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
             #GetFirstChild returns a tuple\r
@@ -574,33 +638,6 @@ class HookeFrame (wx.Frame):
                 self.UpdateNote()\r
                 self.UpdatePlot()\r
 \r
-    def _previous_curve(self, *args):\r
-        '''\r
-        PREVIOUS\r
-        Go to the previous curve in the playlist.\r
-        If we are at the first curve, we jump to the last.\r
-        -------\r
-        Syntax: previous, p\r
-        '''\r
-        #playlist = self.playlists[self.GetActivePlaylistName()][0]\r
-        #select the previous curve and tell the user if we wrapped around\r
-        #self.AppendToOutput(playlist.previous())\r
-        selected_item = self._c['playlists']._c['tree'].GetSelection()\r
-        if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
-            previous_item = self._c['playlists']._c['tree'].GetLastChild(selected_item)\r
-        else:\r
-            previous_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
-            if not previous_item.IsOk():\r
-                parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)\r
-                previous_item = self._c['playlists']._c['tree'].GetLastChild(parent_item)\r
-        self._c['playlists']._c['tree'].SelectItem(previous_item, True)\r
-        playlist = self.GetActivePlaylist()\r
-        if playlist.count > 1:\r
-            playlist.previous()\r
-            self._c['status bar'].set_playlist(playlist)\r
-            self.UpdateNote()\r
-            self.UpdatePlot()\r
-\r
     def _on_notebook_page_close(self, event):\r
         ctrl = event.GetEventObject()\r
         playlist_name = ctrl.GetPageText(ctrl._curpage)\r
@@ -642,7 +679,7 @@ class HookeFrame (wx.Frame):
 \r
     def _on_view(self, event):\r
         menu_id = event.GetId()\r
-        menu_item = self.MenuBar.FindItemById(menu_id)\r
+        menu_item = self._c['menu bar'].FindItemById(menu_id)\r
         menu_label = menu_item.GetLabel()\r
 \r
         pane = self._c['manager'].GetPane(menu_label)\r
diff --git a/hooke/ui/gui/dialog/__init__.py b/hooke/ui/gui/dialog/__init__.py
new file mode 100644 (file)
index 0000000..0d03794
--- /dev/null
@@ -0,0 +1,39 @@
+# Copyright\r
+\r
+from ....util.pluggable import IsSubclass, construct_graph\r
+\r
+\r
+HANDLER_MODULES = [\r
+    'boolean',\r
+    'float',\r
+#    'int'\r
+#    'point',\r
+    'selection',\r
+    'string'\r
+    ]\r
+"""List of handler modules.  TODO: autodiscovery\r
+"""\r
+\r
+class Handler (object):\r
+    """Base class for :class:`~hooke.interaction.Request` handlers.\r
+    \r
+    :attr:`name` identifies the request type and should match the\r
+    module name.\r
+    """\r
+    def __init__(self, name):\r
+        self.name = name\r
+\r
+    def run(self, hooke_frame, msg):\r
+        raise NotImplemented\r
+\r
+    def _cancel(self, *args, **kwargs):\r
+        # TODO: somehow abort the running command\r
+\r
+\r
+HANDLERS = construct_odict(\r
+    this_modname=__name__,\r
+    submodnames=USER_INTERFACE_MODULES,\r
+    class_selector=IsSubclass(UserInterface, blacklist=[UserInterface]))\r
+""":class:`hooke.compat.odict.odict` of :class:`Handler`\r
+instances keyed by `.name`.\r
+"""\r
similarity index 57%
rename from hooke/ui/gui/panel/selection.py
rename to hooke/ui/gui/dialog/selection.py
index 4979ad444704714d3304883d9b9d2b999f48a5da..15baec2cccee822f93ac00d5d37ef4b8769daaff 100644 (file)
@@ -7,6 +7,8 @@ from os import remove
 \r
 import wx\r
 \r
+from ....util.callback import callback, in_callback\r
+\r
 \r
 class Selection (wx.Dialog):\r
     """A selection dialog box.\r
@@ -20,22 +22,34 @@ class Selection (wx.Dialog):
     .. _standard wx IDs:\r
       http://docs.wxwidgets.org/stable/wx_stdevtid.html#stdevtid\r
     """\r
-    def __init__(self, options, message, button_id, button_callback, *args, **kwargs):\r
+    def __init__(self, options, message, button_id, callbacks,\r
+                 default=None, selection_style='single', *args, **kwargs):\r
         super(Selection, self).__init__(*args, **kwargs)\r
 \r
-        self._button_callback = button_callback\r
+        self._options = options\r
+        self._callbacks = callbacks\r
+        self._selection_style = selection_style\r
 \r
         self._c = {\r
             'text': wx.StaticText(\r
                 parent=self, label=message, style=wx.ALIGN_CENTRE),\r
-            'listbox': wx.CheckListBox(\r
-                parent=self, size=wx.Size(175, 200), list=options),\r
             'button': wx.Button(parent=self, id=button_id),\r
             'cancel': wx.Button(self, wx.ID_CANCEL),\r
             }\r
-        self.Bind(wx.EVT_CHECKLISTBOX, self._on_check, self._c['listbox'])\r
-        self.Bind(wx.EVT_BUTTON, self._on_button, self._c['button'])\r
-        self.Bind(wx.EVT_BUTTON, self._on_cancel, self._c['cancel'])\r
+        size = wx.Size(175, 200)\r
+        if selection_style == 'single':\r
+            self._c['listbox'] = wx.ListBox(\r
+                parent=self, size=size, list=options)\r
+            if default != None:\r
+                self._c['listbox'].SetSelection(default)\r
+        else:\r
+            assert selection_style == 'multiple', selection_style\r
+            self._c['listbox'] = wx.CheckListBox(\r
+                parent=self, size=size, list=options)\r
+            if default != None:\r
+                self._c['listbox'].Check(default)\r
+        self.Bind(wx.EVT_BUTTON, self.button, self._c['button'])\r
+        self.Bind(wx.EVT_BUTTON, self.cancel, self._c['cancel'])\r
 \r
         border_width = 5\r
 \r
@@ -65,22 +79,19 @@ class Selection (wx.Dialog):
         self.SetSizer(v)\r
         v.Fit(self)\r
 \r
-    def _on_check(self, event):\r
-        """Refocus on the first checked item.\r
-        """\r
-        index = event.GetSelection()\r
-        self.listbox.SetSelection(index)\r
-\r
-    def _on_cancel(self, event):\r
+    @callback\r
+    def cancel(self, event):\r
         """Close the dialog.\r
         """\r
         self.EndModal(wx.ID_CANCEL)\r
 \r
-    def _on_button(self, event):\r
+    def button(self, event):\r
         """Call ._button_callback() and close the dialog.\r
         """\r
-        self._button_callback(\r
-            event=event,\r
-            items=self._c['listbox'].GetItems(),\r
-            selected_items=self._c['listbox'].GetChecked())\r
+        if self._selection_style == 'single':\r
+            selected = self._c['listbox'].GetSelection()\r
+        else:\r
+            assert self._selection_style == 'multiple', self._selection_style\r
+            selected = self._c['listbox'].GetChecked())\r
+        in_callback(self, options=self._options, selected=selected)\r
         self.EndModal(wx.ID_CLOSE)\r
diff --git a/hooke/ui/gui/dialog/string.py b/hooke/ui/gui/dialog/string.py
new file mode 100644 (file)
index 0000000..6e29ba7
--- /dev/null
@@ -0,0 +1,50 @@
+class StringPopup (wx.Dialog):
+
+        self._c = {
+            'text': wx.StaticText(
+                parent=self, label=message, style=wx.ALIGN_CENTRE),
+            'button': wx.Button(parent=self, id=button_id),
+            'cancel': wx.Button(self, wx.ID_CANCEL),
+            }
+        size = wx.Size(175, 200)
+        if selection_style == 'single':
+            self._c['listbox'] = wx.ListBox(
+                parent=self, size=size, list=options)
+            if default != None:
+                self._c['listbox'].SetSelection(default)
+        else:
+            assert selection_style == 'multiple', selection_style
+            self._c['listbox'] = wx.CheckListBox(
+                parent=self, size=size, list=options)
+            if default != None:
+                self._c['listbox'].Check(default)
+        self.Bind(wx.EVT_BUTTON, self.button, self._c['button'])
+        self.Bind(wx.EVT_BUTTON, self.cancel, self._c['cancel'])
+
+        border_width = 5
+
+        b = wx.BoxSizer(wx.HORIZONTAL)
+        b.Add(window=self._c['button'],
+              flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
+              border=border_width)
+        b.Add(window=self._c['cancel'],
+              flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
+              border=border_width)
+
+        v = wx.BoxSizer(wx.VERTICAL)
+        v.Add(window=self._c['text'],
+              flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
+              border=border_width)
+        v.Add(window=self._c['listbox'],
+              proportion=1,
+              flag=wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL,
+              border=border_width)
+        v.Add(window=wx.StaticLine(
+                parent=self, size=(20,-1), style=wx.LI_HORIZONTAL),
+              flag=wx.GROW,
+              border=border_width)
+        v.Add(window=b,
+              flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER_HORIZONTAL|wx.ALL,
+              border=border_width)
+        self.SetSizer(v)
+        v.Fit(self)
diff --git a/hooke/ui/gui/handler/__init__.py b/hooke/ui/gui/handler/__init__.py
new file mode 100644 (file)
index 0000000..0d03794
--- /dev/null
@@ -0,0 +1,39 @@
+# Copyright\r
+\r
+from ....util.pluggable import IsSubclass, construct_graph\r
+\r
+\r
+HANDLER_MODULES = [\r
+    'boolean',\r
+    'float',\r
+#    'int'\r
+#    'point',\r
+    'selection',\r
+    'string'\r
+    ]\r
+"""List of handler modules.  TODO: autodiscovery\r
+"""\r
+\r
+class Handler (object):\r
+    """Base class for :class:`~hooke.interaction.Request` handlers.\r
+    \r
+    :attr:`name` identifies the request type and should match the\r
+    module name.\r
+    """\r
+    def __init__(self, name):\r
+        self.name = name\r
+\r
+    def run(self, hooke_frame, msg):\r
+        raise NotImplemented\r
+\r
+    def _cancel(self, *args, **kwargs):\r
+        # TODO: somehow abort the running command\r
+\r
+\r
+HANDLERS = construct_odict(\r
+    this_modname=__name__,\r
+    submodnames=USER_INTERFACE_MODULES,\r
+    class_selector=IsSubclass(UserInterface, blacklist=[UserInterface]))\r
+""":class:`hooke.compat.odict.odict` of :class:`Handler`\r
+instances keyed by `.name`.\r
+"""\r
diff --git a/hooke/ui/gui/handler/boolean.py b/hooke/ui/gui/handler/boolean.py
new file mode 100644 (file)
index 0000000..56404d7
--- /dev/null
@@ -0,0 +1,23 @@
+# Copyright
+
+import wx
+
+from . import Handler
+
+
+class BooleanHandler (Handler):
+    
+    def run(self, hooke_frame, msg):
+        if msg.default == True:
+            default = wx.YES_DEFAULT
+        else:
+            default = wx.NO_DEFAULT
+        dialog = wx.MessageDialog(
+            parent=self,
+            message=msg.msg,
+            caption='Boolean Handler',
+            style=swx.YES_NO|default)
+        dialog.ShowModal()
+        dialog.Destroy()
+        return value
+
diff --git a/hooke/ui/gui/handler/float.py b/hooke/ui/gui/handler/float.py
new file mode 100644 (file)
index 0000000..b2e1ba0
--- /dev/null
@@ -0,0 +1,6 @@
+    def _float_request_prompt(self, msg):
+        return self._string_request_prompt(msg)
+
+    def _float_request_parser(self, msg, resposne):
+        return float(response)
+
diff --git a/hooke/ui/gui/handler/selection.py b/hooke/ui/gui/handler/selection.py
new file mode 100644 (file)
index 0000000..89ef426
--- /dev/null
@@ -0,0 +1,37 @@
+# Copyright
+
+"""Define :class:`SelectionHandler` to handle
+:class:`~hooke.interaction.SelectionRequest`\s.
+"""
+
+import wx
+
+from ..dialog.selection import SelectionDialog
+from . import Handler
+
+
+class SelectionHandler (Handler):
+    def __init__(self):
+        super(StringHandler, self).__init__(name='selection')
+
+    def run(self, hooke_frame, msg):
+        self._canceled = True
+        while self._canceled:
+            s = SelectionDialog(
+                options=msg.options,
+                message=msg.msg,
+                button_id=wxID_OK,
+                callbacks={
+                    'button': self._selection,
+                    },
+                default=msg.default,
+                selection_style='single',
+                parent=self,
+                label='Selection handler',
+                style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER),
+            )
+        return self._selected
+
+    def _selection(self, _class, method, options, selected):
+        self._selected = selected
+        self._canceled = False
diff --git a/hooke/ui/gui/handler/string.py b/hooke/ui/gui/handler/string.py
new file mode 100644 (file)
index 0000000..fe2d7e3
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright
+
+"""Define :class:`StringHandler` to handle
+:class:`~hooke.interaction.StringRequest`\s.
+"""
+
+import wx
+
+from . import Handler
+
+
+
+
+class StringHandler (Handler):
+    def __init__(self):
+        super(StringHandler, self).__init__(name='string')
+
+    def run(self, hooke_frame, msg):
+        pass
+
+    def _string_request_prompt(self, msg):
+        if msg.default == None:
+            d = ' '
+        else:
+            d = ' [%s] ' % msg.default
+        return msg.msg + d
+
+    def _string_request_parser(self, msg, response):
+        return response.strip()
+
index 184e66d5de3995363b8333fb51f0542201d4a69e..836bcec92e5ef02018eaf61e03e8585b705ed98c 100644 (file)
@@ -5,44 +5,87 @@
 
 import wx
 
+from ...util.callback import callback, in_callback
+from . import panel as panel
 
-class FileMenu (wx.Menu):
-    def __init__(self, *args, **kwargs):
-        super(FileMenu, self).__init__(*args, **kwargs)
+
+class Menu (wx.Menu):
+    """A `Bind`able version of :class:`wx.Menu`.
+
+    From the `wxPython Style Guide`_, you can't do
+    wx.Menu().Bind(...), so we hack around it by bubbling the Bind up
+    to the closest parent :class:`wx.Frame`.
+
+    .. _wxPython Style Guide:
+      http://wiki.wxpython.org/wxPython%20Style%20Guide#line-101
+    """
+    def __init__(self, parent=None, **kwargs):
+        self._parent = parent
+        super(Menu, self).__init__(**kwargs)
+
+    def Bind(self, **kwargs):
+        assert 'id' in kwargs, kwargs
+        obj = self
+        while not isinstance(obj, wx.Frame):
+            obj = obj._parent
+        obj.Bind(**kwargs)
+
+
+class MenuBar (wx.MenuBar):
+    """A `Bind`able version of :class:`wx.MenuBar`.
+
+    See :class:`Menu` for the motivation.
+    """
+    def __init__(self, parent=None, **kwargs):
+        self._parent = parent
+        super(MenuBar, self).__init__(**kwargs)
+
+    def Append(self, menu, title):
+        menu._parent = self
+        super(MenuBar, self).Append(menu, title)
+
+
+class FileMenu (Menu):
+    def __init__(self, callbacks=None, **kwargs):
+        super(FileMenu, self).__init__(**kwargs)
+        if callbacks == None:
+            callbacks = {}
+        self._callbacks = callbacks
         self._c = {'exit': self.Append(wx.ID_EXIT)}
+        self.Bind(event=wx.EVT_MENU, handler=self.close, id=wx.ID_EXIT)
 
+    @callback
+    def close(self, event):
+        pass
 
-class ViewMenu (wx.Menu):
-    def __init__(self, *args, **kwargs):
-        super(ViewMenu, self).__init__(*args, **kwargs)
-        self._c = {
-            'folders': self.AppendCheckItem(id=wx.ID_ANY, text='Folders\tF5'),
-            'playlist': self.AppendCheckItem(
-                id=wx.ID_ANY, text='Playlists\tF6'),
-            'commands': self.AppendCheckItem(
-                id=wx.ID_ANY, text='Commands\tF7'),
-            'assistant': self.AppendCheckItem(
-                id=wx.ID_ANY, text='Assistant\tF9'),
-            'properties': self.AppendCheckItem(
-                id=wx.ID_ANY, text='Properties\tF8'),
-            'results': self.AppendCheckItem(id=wx.ID_ANY, text='Results\tF10'),
-            'output': self.AppendCheckItem(id=wx.ID_ANY, text='Output\tF11'),
-            'note': self.AppendCheckItem(id=wx.ID_ANY, text='Note\tF12'),
-            }
+
+class ViewMenu (Menu):
+    def __init__(self, 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)
+            self._c[panelname] = self.AppendCheckItem(id=wx.ID_ANY, text=text)
         for item in self._c.values():
             item.Check()
 
 
-class PerspectiveMenu (wx.Menu):
-    def __init__(self, *args, **kwargs):
-        super(PerspectiveMenu, self).__init__(*args, **kwargs)
+class PerspectiveMenu (Menu):
+    def __init__(self, callbacks=None, **kwargs):
+        super(PerspectiveMenu, self).__init__(**kwargs)
+        if callbacks == None:
+            callbacks = {}
+        self._callbacks = callbacks
         self._c = {}
 
     def update(self, perspectives, selected, callback):
         """Rebuild the perspectives menu.
         """
         for item in self.GetMenuItems():
-            self.UnBind(item)
+            self.UnBind(item.GetId)
             self.DeleteItem(item)
         self._c = {
             'save': self.Append(id=wx.ID_ANY, text='Save Perspective'),
@@ -51,27 +94,37 @@ class PerspectiveMenu (wx.Menu):
         self.AppendSeparator()
         for label in perspectives:
             self._c[label] = self.AppendRadioItem(id=wx.ID_ANY, text=label)
-            self.Bind(wx.EVT_MENU, callback, self._c[label])
+            self.Bind(event=wx.EVT_MENU, handler=callback,
+                      id=self._c[label].GetId())
             if label == selected:
                 self._c[label].Check(True)
-            
 
-class HelpMenu (wx.Menu):
-    def __init__(self, *args, **kwargs):
-        super(HelpMenu, self).__init__(*args, **kwargs)
-        self._c = {'about':self.Append(id=wx.ID_ABOUT)}
 
+class HelpMenu (Menu):
+    def __init__(self, callbacks=None, **kwargs):
+        super(HelpMenu, self).__init__(**kwargs)
+        if callbacks == None:
+            callbacks = {}
+        self._callbacks = callbacks
+        self._c = {'about': self.Append(id=wx.ID_ABOUT)}
+        self.Bind(event=wx.EVT_MENU, handler=self.about, id=wx.ID_ABOUT)
 
-class MenuBar (wx.MenuBar):
-    def __init__(self, *args, **kwargs):
-        super(MenuBar, self).__init__(*args, **kwargs)
-        self._c = {
-            'file': FileMenu(),
-            'view': ViewMenu(),
-            'perspective': PerspectiveMenu(),
-            'help': HelpMenu(),
-            }
-        self.Append(self._c['file'], 'File')
-        self.Append(self._c['view'], 'View')
-        self.Append(self._c['perspective'], 'Perspective')
-        self.Append(self._c['help'], 'Help')
+    @callback
+    def about(self, event):
+        pass
+
+
+class HookeMenuBar (MenuBar):
+    def __init__(self, callbacks=None, **kwargs):
+        super(HookeMenuBar, self).__init__(**kwargs)
+        if callbacks == None:
+            callbacks = {}
+        self._callbacks = callbacks
+        self._c = {}
+
+        # Attach *Menu() instances
+        for key in ['file', 'view', 'perspective', 'help']:
+            cap_key = key.capitalize()
+            _class = globals()['%sMenu' % cap_key]
+            self._c[key] = _class(parent=self, callbacks=callbacks)
+            self.Append(self._c[key], cap_key)
index c10f6a1629cfb7d321320aead8b411c7f2ffd040..535f0f6ea857f45258b719b61b41d2b961a323d7 100644 (file)
@@ -5,6 +5,8 @@
 
 import wx
 
+from ...util.callback import callback, in_callback
+
 
 class NavBar (wx.ToolBar):
     def __init__(self, callbacks, *args, **kwargs):
index f5427765a02202ca65853abab96a57a2163621f2..8537598a1942b126b27a98161a90b2f172161008 100644 (file)
@@ -1,14 +1,41 @@
 # Copyright\r
 \r
-from . import commands as commands\r
-from . import note as note\r
-from . import notebook as notebook\r
-from . import playlist as playlist\r
-from . import plot as plot\r
-#from . import propertyeditor as propertyeditor\r
-from . import results as results\r
-from . import selection as selection\r
-from . import welcome as welcome\r
-\r
-__all__ = [commands, note, notebook, playlist, plot, #propertyeditor,\r
-           results, selection, welcome]\r
+from ....util.pluggable import IsSubclass, construct_odict\r
+\r
+\r
+PANEL_MODULES = [\r
+    'commands',\r
+#    'note',\r
+#    'notebook',\r
+#    'playlist',\r
+#    'plot',\r
+#    'propertyeditor',\r
+#    'results',\r
+#    'selection',\r
+#    'welcome',\r
+    ]\r
+"""List of panel modules.  TODO: autodiscovery\r
+"""\r
+\r
+class Panel (object):\r
+    """Base class for Hooke GUI panels.\r
+    \r
+    :attr:`name` identifies the request type and should match the\r
+    module name.\r
+    """\r
+    def __init__(self, name=None, callbacks=None, **kwargs):\r
+        super(Panel, self).__init__(**kwargs)\r
+        self.name = name\r
+        if callbacks == None:\r
+            callbacks = {}\r
+        self._callbacks = callbacks\r
+\r
+\r
+PANELS = construct_odict(\r
+    this_modname=__name__,\r
+    submodnames=PANEL_MODULES,\r
+    class_selector=IsSubclass(Panel, blacklist=[Panel]),\r
+    instantiate=False)\r
+""":class:`hooke.compat.odict.odict` of :class:`Panel`\r
+instances keyed by `.name`.\r
+"""\r
index 1d8313ae1812bb694e9f227d3c9c092910bf5678..84084090993c4242fff129c69b1f67beaf47d0e6 100644 (file)
@@ -16,6 +16,7 @@ import types
 import wx\r
 \r
 from ....util.callback import callback, in_callback\r
+from . import Panel\r
 \r
 \r
 class Tree (wx.TreeCtrl):\r
@@ -136,13 +137,14 @@ class Tree (wx.TreeCtrl):
                 in_callback(self, command, args)\r
 \r
 \r
-class Commands (wx.Panel):\r
+class CommandsPanel (Panel, wx.Panel):\r
     """\r
 \r
     `callbacks` is shared with the underlying :class:`Tree`.\r
     """\r
-    def __init__(self, commands, selected, callbacks, *args, **kwargs):\r
-        super(Commands, self).__init__(*args, **kwargs)\r
+    def __init__(self, callbacks=None, commands=None, selected=None, **kwargs):\r
+        super(CommandsPanel, self).__init__(\r
+            name='commands', callbacks=callbacks, **kwargs)\r
         self._c = {\r
             'tree': Tree(\r
                 commands=commands,\r
@@ -155,8 +157,8 @@ class Commands (wx.Panel):
             'execute': wx.Button(self, label='Execute'),\r
             }\r
         sizer = wx.BoxSizer(wx.VERTICAL)\r
-        sizer.Add(self._c['execute'], 0, wx.EXPAND)\r
         sizer.Add(self._c['tree'], 1, wx.EXPAND)\r
+        sizer.Add(self._c['execute'], 0, wx.EXPAND)\r
         self.SetSizer(sizer)\r
         sizer.Fit(self)\r
 \r
index 4df7db827f057ff9cb3277e13cff5a46fd449f80..7dc68480484e79bc8cc8dd9ffc447685b85d1edd 100644 (file)
@@ -5,10 +5,12 @@
 \r
 import wx\r
 \r
-class Note(wx.Panel):\r
+from . import Panel\r
 \r
-    def __init__(self, parent):\r
-        wx.Panel.__init__(self, parent, -1, style=wx.WANTS_CHARS|wx.NO_BORDER, size=(160, 200))\r
+\r
+class NotePanel (Panel, wx.Panel):\r
+    def __init__(self, callbacks=None, **kwargs):\r
+        super(Note, self).__init__(name='note', callbacks=callbacks, **kwargs)\r
 \r
         self.Editor = wx.TextCtrl(self, style=wx.TE_MULTILINE)\r
 \r
index 4534f427dad6f9b7e9c529bd6b88a790fc652dfc..43b8bb7ba59b2b3c9fef4124f805636480188f0c 100644 (file)
@@ -5,17 +5,19 @@
 
 import wx.aui as aui
 
-from .welcome import Welcome
+from . import Panel
+from .welcome import WelcomeWindow
 
 
-class Notebook (aui.AuiNotebook):
-    def __init__(self, *args, **kwargs):
-        super(Notebook, self).__init__(*args, **kwargs)
+class NotebookPanel (Panel, aui.AuiNotebook):
+    def __init__(self, callbacks=None, **kwargs):
+        super(Notebook, self).__init__(
+            name='notebook', callbacks=callbacks, **kwargs)
         self.SetArtProvider(aui.AuiDefaultTabArt())
         #uncomment if we find a nice icon
         #page_bmp = wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16))
         self.AddPage(
-            Welcome(
+            WelcomeWindow(
                 parent=self,
                 size=wx.Size(400, 300)),
             'Welcome')
index 524b0068a255bcaf2c8e73c478443e3d661b7e14..f0532641d6593616c2e78311ecd0beaa3e954a1d 100644 (file)
@@ -11,6 +11,7 @@ import types
 import wx\r
 \r
 from ....util.callback import callback, in_callback\r
+from . import Panel\r
 \r
 \r
 class Menu (wx.Menu):\r
@@ -269,7 +270,7 @@ class Tree (wx.TreeCtrl):
         return playlist_name\r
 \r
 \r
-class Playlist (wx.Panel):\r
+class Playlist (Panel, wx.Panel):\r
     """:class:`wx.Panel` subclass wrapper for :class:`Tree`.\r
     """\r
     def __init__(self, config, callbacks, *args, **kwargs):\r
@@ -292,6 +293,7 @@ class Playlist (wx.Panel):
         sizer.Fit(self)\r
 \r
         # Expose all Tree's public curve/playlist methods directly.\r
+        # Following DRY and the LoD.\r
         for attribute_name in dir(self._c['tree']):\r
             if (attribute_name.startswith('_')\r
                 or 'playlist' not in attribute_name\r
index a9f852733bf00cbe6f8aff7808301becb5a21fc8..5b5a5ae35ce0e9f4f903454b518ba37a48af6433 100644 (file)
@@ -5,10 +5,12 @@
 
 import wx
 
+from . import Panel
 
-class Welcome (wx.html.HtmlWindow):
+
+class WelcomeWindow (wx.html.HtmlWindow):
     def __init__(self, *args, **kwargs):
-        super(Welcome, self).__init__(self, *args, **kwargs)
+        super(WelcomeWindow, self).__init__(self, *args, **kwargs)
         lines = [
             '<h1>Welcome to Hooke</h1>',
             '<h3>Features</h3>',
@@ -25,3 +27,13 @@ class Welcome (wx.html.HtmlWindow):
             'for more information</p>',
             ]
         ctrl.SetPage('\n'.join(lines))
+
+class WelcomePanel (Panel, wx.Panel):
+    def __init__(self, callbacks=None, **kwargs):
+        super(WelcomePanel, self).__init__(
+            name='welcome', callbacks=callbacks, **kwargs)
+        self._c = {
+            'window': WelcomeWindow(
+                parent=self,
+                size=wx.Size(400, 300)),
+            }
index 59e58597c9fe5991b5e64c2c3e4c81e01ed955c2..17de9662d4f75f3b6b9e96603869eafccaba5bd1 100644 (file)
@@ -5,6 +5,8 @@
 
 import wx
 
+from ... import version
+
 
 class StatusBar (wx.StatusBar):
     def __init__(self, *args, **kwargs):
@@ -24,7 +26,7 @@ class StatusBar (wx.StatusBar):
             playlist.name,
             '(%d/%d)' % (playlist._index, len(playlist)),
             ]
-        curve = playlist.current():
+        curve = playlist.current()
         if curve != None:
             fields.append(curve.name)
         return ' '.join(fields)
index 0a491cb67b9491f4719292c86697422a6fd749a6..14eaeb0209ed0f9bd4f6326dd19d85e355df3fe9 100644 (file)
@@ -19,7 +19,8 @@
 """`pluggable`
 """
 
-from ..util.graph import Node, Graph
+from ..compat.odict import odict
+from .graph import Node, Graph
 
 
 class IsSubclass (object):
@@ -58,6 +59,34 @@ class IsSubclass (object):
             return False
         return subclass
 
+
+def construct_odict(this_modname, submodnames, class_selector,
+                    instantiate=True):
+    """Search the submodules `submodnames` of a module `this_modname`
+    for class objects for which `class_selector(class)` returns
+    `True`.  If `instantiate == True` these classes are instantiated
+    and stored in the returned :class:`hooke.compat.odict.odict` in
+    the order in which they were discovered.  Otherwise, the class
+    itself is stored.
+    """
+    objs = odict()
+    for submodname in submodnames:
+        count = len([s for s in submodnames if s == submodname])
+        assert count > 0, 'No %s entries: %s' % (submodname, submodnames)
+        assert count == 1, 'Multiple (%d) %s entries: %s' \
+            % (count, submodname, submodnames)
+        this_mod = __import__(this_modname, fromlist=[submodname])
+        submod = getattr(this_mod, submodname)
+        for objname in dir(submod):
+            obj = getattr(submod, objname)
+            if class_selector(obj):
+                if instantiate == True:
+                    obj = obj()
+                name = getattr(obj, 'name', submodname)
+                objs[name] = obj
+    return objs
+
+
 def construct_graph(this_modname, submodnames, class_selector,
                     assert_name_match=True):
     """Search the submodules `submodnames` of a module `this_modname`