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/
 
 
 .. _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
 ============
 
 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.
     
     """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):
     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 = [
     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)]
 
             DifferenceCommand(self), DerivativeCommand(self),
             PowerSpectrumCommand(self)]
 
@@ -64,6 +64,17 @@ of the current playlist.
 
 # Define commands
 
 
 # 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`.
     """
 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
 import ConfigParser as configparser
 
 from .. import version
-from ..compat.odict import odict
 from ..config import Setting
 from ..config import Setting
-from ..util.pluggable import IsSubclass
+from ..util.pluggable import IsSubclass, construct_odict
 
 
 USER_INTERFACE_MODULES = [
 
 
 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.
     """
     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
         self.command = command
+        if arguments == None:
+            arguments = {}
         self.arguments = arguments
 
 class UserInterface (object):
         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
 
         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__,
 
 USER_INTERFACES = construct_odict(
     this_modname=__name__,
index 69f5a61588a7c698ba9bdbbb45f3a43942b722ae..f83936d0a1c09959f042a6ccecbca58e5af4c95d 100644 (file)
@@ -1,6 +1,6 @@
 # Copyright\r
 \r
 # 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
 """\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
 \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
 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
 \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.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
             parent=self,\r
             style=wx.ST_SIZEGRIP)\r
+        self.SetStatusBar(self._c['status bar'])\r
 \r
         self._update_perspectives()\r
         self._bind_events()\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
     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
                     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
                     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
             #('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
             ]:\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
 \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
         elif style == 'center':\r
-            info.CenterPane().PaneBorder(False)\r
+            info.CenterPane()\r
+        elif style == 'left':\r
+            info.Left()\r
         elif style == 'right':\r
         elif style == 'right':\r
-            info.Right().CloseButton(True).MaximizeButton(False)\r
+            info.Right()\r
         else:\r
             assert style == 'bottom', style\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['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
             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['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
             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_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
         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
         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
         #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
     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
         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
         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
         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
             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
             return menu_item\r
         else:\r
             return None\r
@@ -389,17 +434,16 @@ class HookeFrame (wx.Frame):
                 return True\r
         return False\r
 \r
                 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
         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
         # 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
             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
         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
             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
             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
             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
         # ) 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
                 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
             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
         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
         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
                 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
     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
 \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
         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
 \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
 \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
     .. _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
         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
 \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
             '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
 \r
         border_width = 5\r
 \r
@@ -65,22 +79,19 @@ class Selection (wx.Dialog):
         self.SetSizer(v)\r
         v.Fit(self)\r
 \r
         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
         """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
         """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
         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
 
 
 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._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()
 
 
         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._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'),
             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.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)
             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
 
 
 import wx
 
+from ...util.callback import callback, in_callback
+
 
 class NavBar (wx.ToolBar):
     def __init__(self, callbacks, *args, **kwargs):
 
 class NavBar (wx.ToolBar):
     def __init__(self, callbacks, *args, **kwargs):
index f5427765a02202ca65853abab96a57a2163621f2..8537598a1942b126b27a98161a90b2f172161008 100644 (file)
@@ -1,14 +1,41 @@
 # Copyright\r
 \r
 # 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
 import wx\r
 \r
 from ....util.callback import callback, in_callback\r
+from . import Panel\r
 \r
 \r
 class Tree (wx.TreeCtrl):\r
 \r
 \r
 class Tree (wx.TreeCtrl):\r
@@ -136,13 +137,14 @@ class Tree (wx.TreeCtrl):
                 in_callback(self, command, args)\r
 \r
 \r
                 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
     """\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
         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
             '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['tree'], 1, wx.EXPAND)\r
+        sizer.Add(self._c['execute'], 0, wx.EXPAND)\r
         self.SetSizer(sizer)\r
         sizer.Fit(self)\r
 \r
         self.SetSizer(sizer)\r
         sizer.Fit(self)\r
 \r
index 4df7db827f057ff9cb3277e13cff5a46fd449f80..7dc68480484e79bc8cc8dd9ffc447685b85d1edd 100644 (file)
@@ -5,10 +5,12 @@
 \r
 import wx\r
 \r
 \r
 import wx\r
 \r
-class Note(wx.Panel):\r
+from . import Panel\r
 \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
 \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
 
 
 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(
         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')
                 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
 import wx\r
 \r
 from ....util.callback import callback, in_callback\r
+from . import Panel\r
 \r
 \r
 class Menu (wx.Menu):\r
 \r
 \r
 class Menu (wx.Menu):\r
@@ -269,7 +270,7 @@ class Tree (wx.TreeCtrl):
         return playlist_name\r
 \r
 \r
         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
     """: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
         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
         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
 
 
 import wx
 
+from . import Panel
 
 
-class Welcome (wx.html.HtmlWindow):
+
+class WelcomeWindow (wx.html.HtmlWindow):
     def __init__(self, *args, **kwargs):
     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>',
         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))
             '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
 
 
 import wx
 
+from ... import version
+
 
 class StatusBar (wx.StatusBar):
     def __init__(self, *args, **kwargs):
 
 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)),
             ]
             playlist.name,
             '(%d/%d)' % (playlist._index, len(playlist)),
             ]
-        curve = playlist.current():
+        curve = playlist.current()
         if curve != None:
             fields.append(curve.name)
         return ' '.join(fields)
         if curve != None:
             fields.append(curve.name)
         return ' '.join(fields)
index 0a491cb67b9491f4719292c86697422a6fd749a6..14eaeb0209ed0f9bd4f6326dd19d85e355df3fe9 100644 (file)
@@ -19,7 +19,8 @@
 """`pluggable`
 """
 
 """`pluggable`
 """
 
-from ..util.graph import Node, Graph
+from ..compat.odict import odict
+from .graph import Node, Graph
 
 
 class IsSubclass (object):
 
 
 class IsSubclass (object):
@@ -58,6 +59,34 @@ class IsSubclass (object):
             return False
         return subclass
 
             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`
 def construct_graph(this_modname, submodnames, class_selector,
                     assert_name_match=True):
     """Search the submodules `submodnames` of a module `this_modname`