.. _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
============
"""
-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):
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)]
# 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`.
"""
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 = [
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):
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__,
# 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
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
\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
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
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
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
#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
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
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 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
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
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
# ) 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
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
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
\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
--- /dev/null
+# 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
\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
.. _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
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
--- /dev/null
+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)
--- /dev/null
+# 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
--- /dev/null
+# 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
+
--- /dev/null
+ def _float_request_prompt(self, msg):
+ return self._string_request_prompt(msg)
+
+ def _float_request_parser(self, msg, resposne):
+ return float(response)
+
--- /dev/null
+# 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
--- /dev/null
+# 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()
+
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'),
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)
import wx
+from ...util.callback import callback, in_callback
+
class NavBar (wx.ToolBar):
def __init__(self, callbacks, *args, **kwargs):
# 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
import wx\r
\r
from ....util.callback import callback, in_callback\r
+from . import Panel\r
\r
\r
class Tree (wx.TreeCtrl):\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
- 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
'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
\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
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')
import wx\r
\r
from ....util.callback import callback, in_callback\r
+from . import Panel\r
\r
\r
class Menu (wx.Menu):\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
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
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>',
'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)),
+ }
import wx
+from ... import version
+
class StatusBar (wx.StatusBar):
def __init__(self, *args, **kwargs):
playlist.name,
'(%d/%d)' % (playlist._index, len(playlist)),
]
- curve = playlist.current():
+ curve = playlist.current()
if curve != None:
fields.append(curve.name)
return ' '.join(fields)
"""`pluggable`
"""
-from ..util.graph import Node, Graph
+from ..compat.odict import odict
+from .graph import Node, Graph
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`