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

Welcome to Hooke

', '

Features

', @@ -25,3 +27,13 @@ class Welcome (wx.html.HtmlWindow): 'for more information

', ] 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)), + } diff --git a/hooke/ui/gui/statusbar.py b/hooke/ui/gui/statusbar.py index 59e5859..17de966 100644 --- a/hooke/ui/gui/statusbar.py +++ b/hooke/ui/gui/statusbar.py @@ -5,6 +5,8 @@ import wx +from ... import version + class StatusBar (wx.StatusBar): def __init__(self, *args, **kwargs): @@ -24,7 +26,7 @@ class StatusBar (wx.StatusBar): playlist.name, '(%d/%d)' % (playlist._index, len(playlist)), ] - curve = playlist.current(): + curve = playlist.current() if curve != None: fields.append(curve.name) return ' '.join(fields) diff --git a/hooke/util/pluggable.py b/hooke/util/pluggable.py index 0a491cb..14eaeb0 100644 --- a/hooke/util/pluggable.py +++ b/hooke/util/pluggable.py @@ -19,7 +19,8 @@ """`pluggable` """ -from ..util.graph import Node, Graph +from ..compat.odict import odict +from .graph import Node, Graph class IsSubclass (object): @@ -58,6 +59,34 @@ class IsSubclass (object): return False return subclass + +def construct_odict(this_modname, submodnames, class_selector, + instantiate=True): + """Search the submodules `submodnames` of a module `this_modname` + for class objects for which `class_selector(class)` returns + `True`. If `instantiate == True` these classes are instantiated + and stored in the returned :class:`hooke.compat.odict.odict` in + the order in which they were discovered. Otherwise, the class + itself is stored. + """ + objs = odict() + for submodname in submodnames: + count = len([s for s in submodnames if s == submodname]) + assert count > 0, 'No %s entries: %s' % (submodname, submodnames) + assert count == 1, 'Multiple (%d) %s entries: %s' \ + % (count, submodname, submodnames) + this_mod = __import__(this_modname, fromlist=[submodname]) + submod = getattr(this_mod, submodname) + for objname in dir(submod): + obj = getattr(submod, objname) + if class_selector(obj): + if instantiate == True: + obj = obj() + name = getattr(obj, 'name', submodname) + objs[name] = obj + return objs + + def construct_graph(this_modname, submodnames, class_selector, assert_name_match=True): """Search the submodules `submodnames` of a module `this_modname`