More cleanups to Setting/Argument type handling, mostly for count != 1.
[hooke.git] / hooke / ui / gui / __init__.py
1 # Copyright (C) 2008-2010 Fabrizio Benedetti
2 #                         Massimo Sandal <devicerandom@gmail.com>
3 #                         Rolf Schmidt <rschmidt@alcor.concordia.ca>
4 #                         W. Trevor King <wking@drexel.edu>
5 #
6 # This file is part of Hooke.
7 #
8 # Hooke is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU Lesser General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
12 #
13 # Hooke is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
16 # Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with Hooke.  If not, see
20 # <http://www.gnu.org/licenses/>.
21
22 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
23
24 """
25
26 WX_GOOD=['2.8']
27
28 import wxversion
29 wxversion.select(WX_GOOD)
30
31 import copy
32 import logging
33 import os
34 import os.path
35 import platform
36 import shutil
37 import time
38
39 import wx.html
40 import wx.aui as aui
41 import wx.lib.evtmgr as evtmgr
42 # wxPropertyGrid is included in wxPython >= 2.9.1, see
43 #   http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
44 # until then, we'll avoid it because of the *nix build problems.
45 #import wx.propgrid as wxpg
46
47 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
48 from ...config import Setting
49 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
50 from ...ui import UserInterface, CommandMessage
51 from .dialog.selection import Selection as SelectionDialog
52 from .dialog.save_file import select_save_file
53 from . import menu as menu
54 from . import navbar as navbar
55 from . import panel as panel
56 from .panel.propertyeditor import prop_from_argument, prop_from_setting
57 from . import statusbar as statusbar
58
59
60 class HookeFrame (wx.Frame):
61     """The main Hooke-interface window.    
62     """
63     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
64         super(HookeFrame, self).__init__(*args, **kwargs)
65         self.log = logging.getLogger('hooke')
66         self.gui = gui
67         self.commands = commands
68         self.inqueue = inqueue
69         self.outqueue = outqueue
70         self._perspectives = {}  # {name: perspective_str}
71         self._c = {}
72
73         self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
74
75         # setup frame manager
76         self._c['manager'] = aui.AuiManager()
77         self._c['manager'].SetManagedWindow(self)
78
79         # set the gradient and drag styles
80         self._c['manager'].GetArtProvider().SetMetric(
81             aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
82         self._c['manager'].SetFlags(
83             self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
84
85         # Min size for the frame itself isn't completely done.  See
86         # the end of FrameManager::Update() for the test code. For
87         # now, just hard code a frame minimum size.
88         #self.SetMinSize(wx.Size(500, 500))
89
90         self._setup_panels()
91         self._setup_toolbars()
92         self._c['manager'].Update()  # commit pending changes
93
94         # Create the menubar after the panes so that the default
95         # perspective is created with all panes open
96         panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
97         self._c['menu bar'] = menu.HookeMenuBar(
98             parent=self,
99             panels=panels,
100             callbacks={
101                 'close': self._on_close,
102                 'about': self._on_about,
103                 'view_panel': self._on_panel_visibility,
104                 'save_perspective': self._on_save_perspective,
105                 'delete_perspective': self._on_delete_perspective,
106                 'select_perspective': self._on_select_perspective,
107                 })
108         self.SetMenuBar(self._c['menu bar'])
109
110         self._c['status bar'] = statusbar.StatusBar(
111             parent=self,
112             style=wx.ST_SIZEGRIP)
113         self.SetStatusBar(self._c['status bar'])
114
115         self._setup_perspectives()
116         self._bind_events()
117
118         self.execute_command(
119                 command=self._command_by_name('load playlist'),
120                 args={'input':'test/data/vclamp_picoforce/playlist'},
121                 )
122         return # TODO: cleanup
123         self.playlists = self._c['playlist'].Playlists
124         self._displayed_plot = None
125         #load default list, if possible
126         self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
127
128
129     # GUI maintenance
130
131     def _setup_panels(self):
132         client_size = self.GetClientSize()
133         for p,style in [
134 #            ('folders', wx.GenericDirCtrl(
135 #                    parent=self,
136 #                    dir=self.gui.config['folders-workdir'],
137 #                    size=(200, 250),
138 #                    style=wx.DIRCTRL_SHOW_FILTERS,
139 #                    filter=self.gui.config['folders-filters'],
140 #                    defaultFilter=self.gui.config['folders-filter-index']), 'left'),
141             (panel.PANELS['playlist'](
142                     callbacks={
143                         'delete_playlist':self._on_user_delete_playlist,
144                         '_delete_playlist':self._on_delete_playlist,
145                         'delete_curve':self._on_user_delete_curve,
146                         '_delete_curve':self._on_delete_curve,
147                         '_on_set_selected_playlist':self._on_set_selected_playlist,
148                         '_on_set_selected_curve':self._on_set_selected_curve,
149                         },
150                     parent=self,
151                     style=wx.WANTS_CHARS|wx.NO_BORDER,
152                     # WANTS_CHARS so the panel doesn't eat the Return key.
153 #                    size=(160, 200),
154                     ), 'left'),
155             (panel.PANELS['note'](
156                     callbacks = {
157                         '_on_update':self._on_update_note,
158                         },
159                     parent=self,
160                     style=wx.WANTS_CHARS|wx.NO_BORDER,
161 #                    size=(160, 200),
162                     ), 'left'),
163 #            ('notebook', Notebook(
164 #                    parent=self,
165 #                    pos=wx.Point(client_size.x, client_size.y),
166 #                    size=wx.Size(430, 200),
167 #                    style=aui.AUI_NB_DEFAULT_STYLE
168 #                    | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
169             (panel.PANELS['commands'](
170                     commands=self.commands,
171                     selected=self.gui.config['selected command'],
172                     callbacks={
173                         'execute': self.execute_command,
174                         'select_plugin': self.select_plugin,
175                         'select_command': self.select_command,
176 #                        'selection_changed': self.panelProperties.select(self, method, command),  #SelectedTreeItem = selected_item,
177                         },
178                     parent=self,
179                     style=wx.WANTS_CHARS|wx.NO_BORDER,
180                     # WANTS_CHARS so the panel doesn't eat the Return key.
181 #                    size=(160, 200),
182                     ), 'right'),
183             (panel.PANELS['propertyeditor'](
184                     callbacks={},
185                     parent=self,
186                     style=wx.WANTS_CHARS,
187                     # WANTS_CHARS so the panel doesn't eat the Return key.
188                     ), 'center'),
189 #            ('assistant', wx.TextCtrl(
190 #                    parent=self,
191 #                    pos=wx.Point(0, 0),
192 #                    size=wx.Size(150, 90),
193 #                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
194             (panel.PANELS['plot'](
195                     callbacks={
196                         },
197                     parent=self,
198                     style=wx.WANTS_CHARS|wx.NO_BORDER,
199                     # WANTS_CHARS so the panel doesn't eat the Return key.
200 #                    size=(160, 200),
201                     ), 'center'),
202             (panel.PANELS['output'](
203                     parent=self,
204                     pos=wx.Point(0, 0),
205                     size=wx.Size(150, 90),
206                     style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
207              'bottom'),
208 #            ('results', panel.results.Results(self), 'bottom'),
209             ]:
210             self._add_panel(p, style)
211         #self._c['assistant'].SetEditable(False)
212
213     def _add_panel(self, panel, style):
214         self._c[panel.name] = panel
215         m_name = panel.managed_name
216         info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
217         info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
218         if style == 'top':
219             info.Top()
220         elif style == 'center':
221             info.CenterPane()
222         elif style == 'left':
223             info.Left()
224         elif style == 'right':
225             info.Right()
226         else:
227             assert style == 'bottom', style
228             info.Bottom()
229         self._c['manager'].AddPane(panel, info)
230
231     def _setup_toolbars(self):
232         self._c['navigation bar'] = navbar.NavBar(
233             callbacks={
234                 'next': self._next_curve,
235                 'previous': self._previous_curve,
236                 },
237             parent=self,
238             style=wx.TB_FLAT | wx.TB_NODIVIDER)
239         self._c['manager'].AddPane(
240             self._c['navigation bar'],
241             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
242                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
243                 ).RightDockable(False))
244
245     def _bind_events(self):
246         # TODO: figure out if we can use the eventManager for menu
247         # ranges and events of 'self' without raising an assertion
248         # fail error.
249         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
250         self.Bind(wx.EVT_SIZE, self._on_size)
251         self.Bind(wx.EVT_CLOSE, self._on_close)
252         self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
253         self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
254
255         return # TODO: cleanup
256         treeCtrl = self._c['folders'].GetTreeCtrl()
257         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
258         
259         #property editor
260         self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
261         #results panel
262         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
263
264     def _on_about(self, *args):
265         dialog = wx.MessageDialog(
266             parent=self,
267             message=self.gui._splash_text(extra_info={
268                     'get-details':'click "Help -> License"'},
269                                           wrap=False),
270             caption='About Hooke',
271             style=wx.OK|wx.ICON_INFORMATION)
272         dialog.ShowModal()
273         dialog.Destroy()
274
275     def _on_close(self, *args):
276         self.log.info('closing GUI framework')
277         # apply changes
278         self.gui.config['main height'] = str(self.GetSize().GetHeight())
279         self.gui.config['main left'] = str(self.GetPosition()[0])
280         self.gui.config['main top'] = str(self.GetPosition()[1])
281         self.gui.config['main width'] = str(self.GetSize().GetWidth())
282         # push changes back to Hooke.config?
283         self._c['manager'].UnInit()
284         del self._c['manager']
285         self.Destroy()
286
287
288
289     # Panel utility functions
290
291     def _file_name(self, name):
292         """Cleanup names according to configured preferences.
293         """
294         if self.gui.config['hide extensions'] == True:
295             name,ext = os.path.splitext(name)
296         return name
297
298
299
300     # Command handling
301
302     def _command_by_name(self, name):
303         cs = [c for c in self.commands if c.name == name]
304         if len(cs) == 0:
305             raise KeyError(name)
306         elif len(cs) > 1:
307             raise Exception('Multiple commands named "%s"' % name)
308         return cs[0]
309
310     def execute_command(self, _class=None, method=None,
311                         command=None, args=None):
312         if args == None:
313             args = {}
314         if ('property editor' in self._c
315             and self.gui.config['selected command'] == command):
316             arg_names = [arg.name for arg in command.arguments]
317             for name,value in self._c['property editor'].get_values().items():
318                 if name in arg_names:
319                     args[name] = value
320         self.log.debug('executing %s with %s' % (command.name, args))
321         self.inqueue.put(CommandMessage(command, args))
322         results = []
323         while True:
324             msg = self.outqueue.get()
325             results.append(msg)
326             if isinstance(msg, Exit):
327                 self._on_close()
328                 break
329             elif isinstance(msg, CommandExit):
330                 # TODO: display command complete
331                 break
332             elif isinstance(msg, ReloadUserInterfaceConfig):
333                 self.gui.reload_config(msg.config)
334                 continue
335             elif isinstance(msg, Request):
336                 h = handler.HANDLERS[msg.type]
337                 h.run(self, msg)  # TODO: pause for response?
338                 continue
339         pp = getattr(
340             self, '_postprocess_%s' % command.name.replace(' ', '_'),
341             self._postprocess_text)
342         pp(command=command, args=args, results=results)
343         return results
344
345     def _handle_request(self, msg):
346         """Repeatedly try to get a response to `msg`.
347         """
348         if prompt == None:
349             raise NotImplementedError('_%s_request_prompt' % msg.type)
350         prompt_string = prompt(msg)
351         parser = getattr(self, '_%s_request_parser' % msg.type, None)
352         if parser == None:
353             raise NotImplementedError('_%s_request_parser' % msg.type)
354         error = None
355         while True:
356             if error != None:
357                 self.cmd.stdout.write(''.join([
358                         error.__class__.__name__, ': ', str(error), '\n']))
359             self.cmd.stdout.write(prompt_string)
360             value = parser(msg, self.cmd.stdin.readline())
361             try:
362                 response = msg.response(value)
363                 break
364             except ValueError, error:
365                 continue
366         self.inqueue.put(response)
367
368
369
370     # Command-specific postprocessing
371
372     def _postprocess_text(self, command, args={}, results=[]):
373         """Print the string representation of the results to the Results window.
374
375         This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
376         approach, except that :class:`~hooke.ui.commandline.DoCommand`
377         doesn't print some internally handled messages
378         (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
379         """
380         for result in results:
381             if isinstance(result, CommandExit):
382                 self._c['output'].write(result.__class__.__name__+'\n')
383             self._c['output'].write(str(result).rstrip()+'\n')
384
385     def _postprocess_load_playlist(self, command, args={}, results=None):
386         """Update `self` to show the playlist.
387         """
388         if not isinstance(results[-1], Success):
389             self._postprocess_text(command, results=results)
390             return
391         assert len(results) == 2, results
392         playlist = results[0]
393         self._c['playlist']._c['tree'].add_playlist(playlist)
394
395     def _postprocess_get_playlist(self, command, args={}, results=[]):
396         if not isinstance(results[-1], Success):
397             self._postprocess_text(command, results=results)
398             return
399         assert len(results) == 2, results
400         playlist = results[0]
401         self._c['playlist']._c['tree'].update_playlist(playlist)
402
403     def _postprocess_get_curve(self, command, args={}, results=[]):
404         """Update `self` to show the curve.
405         """
406         if not isinstance(results[-1], Success):
407             self._postprocess_text(command, results=results)
408             return
409         assert len(results) == 2, results
410         curve = results[0]
411         if args.get('curve', None) == None:
412             # the command defaults to the current curve of the current playlist
413             results = self.execute_command(
414                 command=self._command_by_name('get playlist'))
415             playlist = results[0]
416         else:
417             raise NotImplementedError()
418         if 'note' in self._c:
419             self._c['note'].set_text(curve.info['note'])
420         if 'playlist' in self._c:
421             self._c['playlist']._c['tree'].set_selected_curve(
422                 playlist, curve)
423         if 'plot' in self._c:
424             self._c['plot'].set_curve(curve, config=self.gui.config)
425
426     def _postprocess_next_curve(self, command, args={}, results=[]):
427         """No-op.  Only call 'next curve' via `self._next_curve()`.
428         """
429         pass
430
431     def _postprocess_previous_curve(self, command, args={}, results=[]):
432         """No-op.  Only call 'previous curve' via `self._previous_curve()`.
433         """
434         pass
435
436     def _postprocess_zero_block_surface_contact_point(
437         self, command, args={}, results=[]):
438         """Update the curve, since the available columns may have changed.
439         """
440         if isinstance(results[-1], Success):
441             self.execute_command(
442                 command=self._command_by_name('get curve'))
443  
444     def _postprocess_add_block_force_array(
445         self, command, args={}, results=[]):
446         """Update the curve, since the available columns may have changed.
447         """
448         if isinstance(results[-1], Success):
449             self.execute_command(
450                 command=self._command_by_name('get curve'))
451
452
453
454     # TODO: cruft
455
456     def _GetActiveFileIndex(self):
457         lib.playlist.Playlist = self.GetActivePlaylist()
458         #get the selected item from the tree
459         selected_item = self._c['playlist']._c['tree'].GetSelection()
460         #test if a playlist or a curve was double-clicked
461         if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
462             return -1
463         else:
464             count = 0
465             selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
466             while selected_item.IsOk():
467                 count += 1
468                 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
469             return count
470
471     def _GetPlaylistTab(self, name):
472         for index, page in enumerate(self._c['notebook']._tabs._pages):
473             if page.caption == name:
474                 return index
475         return -1
476
477     def select_plugin(self, _class=None, method=None, plugin=None):
478         pass
479
480     def AddPlaylistFromFiles(self, files=[], name='Untitled'):
481         if files:
482             playlist = lib.playlist.Playlist(self, self.drivers)
483             for item in files:
484                 playlist.add_curve(item)
485         if playlist.count > 0:
486             playlist.name = self._GetUniquePlaylistName(name)
487             playlist.reset()
488             self.AddTayliss(playlist)
489
490     def AppliesPlotmanipulator(self, name):
491         '''
492         Returns True if the plotmanipulator 'name' is applied, False otherwise
493         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
494         '''
495         return self.GetBoolFromConfig('core', 'plotmanipulators', name)
496
497     def ApplyPlotmanipulators(self, plot, plot_file):
498         '''
499         Apply all active plotmanipulators.
500         '''
501         if plot is not None and plot_file is not None:
502             manipulated_plot = copy.deepcopy(plot)
503             for plotmanipulator in self.plotmanipulators:
504                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
505                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
506             return manipulated_plot
507
508     def GetActiveFigure(self):
509         playlist_name = self.GetActivePlaylistName()
510         figure = self.playlists[playlist_name].figure
511         if figure is not None:
512             return figure
513         return None
514
515     def GetActiveFile(self):
516         playlist = self.GetActivePlaylist()
517         if playlist is not None:
518             return playlist.get_active_file()
519         return None
520
521     def GetActivePlot(self):
522         playlist = self.GetActivePlaylist()
523         if playlist is not None:
524             return playlist.get_active_file().plot
525         return None
526
527     def GetDisplayedPlot(self):
528         plot = copy.deepcopy(self.displayed_plot)
529         #plot.curves = []
530         #plot.curves = copy.deepcopy(plot.curves)
531         return plot
532
533     def GetDisplayedPlotCorrected(self):
534         plot = copy.deepcopy(self.displayed_plot)
535         plot.curves = []
536         plot.curves = copy.deepcopy(plot.corrected_curves)
537         return plot
538
539     def GetDisplayedPlotRaw(self):
540         plot = copy.deepcopy(self.displayed_plot)
541         plot.curves = []
542         plot.curves = copy.deepcopy(plot.raw_curves)
543         return plot
544
545     def GetDockArt(self):
546         return self._c['manager'].GetArtProvider()
547
548     def GetPlotmanipulator(self, name):
549         '''
550         Returns a plot manipulator function from its name
551         '''
552         for plotmanipulator in self.plotmanipulators:
553             if plotmanipulator.name == name:
554                 return plotmanipulator
555         return None
556
557     def HasPlotmanipulator(self, name):
558         '''
559         returns True if the plotmanipulator 'name' is loaded, False otherwise
560         '''
561         for plotmanipulator in self.plotmanipulators:
562             if plotmanipulator.command == name:
563                 return True
564         return False
565
566
567     def _on_dir_ctrl_left_double_click(self, event):
568         file_path = self.panelFolders.GetPath()
569         if os.path.isfile(file_path):
570             if file_path.endswith('.hkp'):
571                 self.do_loadlist(file_path)
572         event.Skip()
573
574     def _on_erase_background(self, event):
575         event.Skip()
576
577     def _on_notebook_page_close(self, event):
578         ctrl = event.GetEventObject()
579         playlist_name = ctrl.GetPageText(ctrl._curpage)
580         self.DeleteFromPlaylists(playlist_name)
581
582     def OnPaneClose(self, event):
583         event.Skip()
584
585     def OnPropGridChanged (self, event):
586         prop = event.GetProperty()
587         if prop:
588             item_section = self.panelProperties.SelectedTreeItem
589             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
590             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
591             config = self.gui.config[plugin]
592             property_section = self._c['commands']._c['tree'].GetItemText(item_section)
593             property_key = prop.GetName()
594             property_value = prop.GetDisplayedString()
595
596             config[property_section][property_key]['value'] = property_value
597
598     def OnResultsCheck(self, index, flag):
599         results = self.GetActivePlot().results
600         if results.has_key(self.results_str):
601             results[self.results_str].results[index].visible = flag
602             results[self.results_str].update()
603             self.UpdatePlot()
604
605
606     def _on_size(self, event):
607         event.Skip()
608
609     def UpdatePlaylistsTreeSelection(self):
610         playlist = self.GetActivePlaylist()
611         if playlist is not None:
612             if playlist.index >= 0:
613                 self._c['status bar'].set_playlist(playlist)
614                 self.UpdateNote()
615                 self.UpdatePlot()
616
617     def _on_curve_select(self, playlist, curve):
618         #create the plot tab and add playlist to the dictionary
619         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
620         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
621         #tab_index = self._c['notebook'].GetSelection()
622         playlist.figure = plotPanel.get_figure()
623         self.playlists[playlist.name] = playlist
624         #self.playlists[playlist.name] = [playlist, figure]
625         self._c['status bar'].set_playlist(playlist)
626         self.UpdateNote()
627         self.UpdatePlot()
628
629
630     def _on_playlist_left_doubleclick(self):
631         index = self._c['notebook'].GetSelection()
632         current_playlist = self._c['notebook'].GetPageText(index)
633         if current_playlist != playlist_name:
634             index = self._GetPlaylistTab(playlist_name)
635             self._c['notebook'].SetSelection(index)
636         self._c['status bar'].set_playlist(playlist)
637         self.UpdateNote()
638         self.UpdatePlot()
639
640     def _on_playlist_delete(self, playlist):
641         notebook = self.Parent.plotNotebook
642         index = self.Parent._GetPlaylistTab(playlist.name)
643         notebook.SetSelection(index)
644         notebook.DeletePage(notebook.GetSelection())
645         self.Parent.DeleteFromPlaylists(playlist_name)
646
647
648
649     # Command panel interface
650
651     def select_command(self, _class, method, command):
652         #self.select_plugin(plugin=command.plugin)
653         if 'assistant' in self._c:
654             self._c['assitant'].ChangeValue(command.help)
655         self._c['property editor'].clear()
656         for argument in command.arguments:
657             if argument.name == 'help':
658                 continue
659
660             results = self.execute_command(
661                 command=self._command_by_name('playlists'))
662             if not isinstance(results[-1], Success):
663                 self._postprocess_text(command, results=results)
664                 playlists = []
665             else:
666                 playlists = results[0]
667
668             results = self.execute_command(
669                 command=self._command_by_name('playlist curves'))
670             if not isinstance(results[-1], Success):
671                 self._postprocess_text(command, results=results)
672                 curves = []
673             else:
674                 curves = results[0]
675
676             p = prop_from_argument(
677                 argument, curves=curves, playlists=playlists)
678             if p == None:
679                 continue  # property intentionally not handled (yet)
680             self._c['property editor'].append_property(p)
681
682         self.gui.config['selected command'] = command  # TODO: push to engine
683
684
685
686     # Note panel interface
687
688     def _on_update_note(self, _class, method, text):
689         """Sets the note for the active curve.
690         """
691         self.execute_command(
692             command=self._command_by_name('set note'),
693             args={'note':text})
694
695
696
697     # Playlist panel interface
698
699     def _on_user_delete_playlist(self, _class, method, playlist):
700         pass
701
702     def _on_delete_playlist(self, _class, method, playlist):
703         if hasattr(playlist, 'path') and playlist.path != None:
704             os.remove(playlist.path)
705
706     def _on_user_delete_curve(self, _class, method, playlist, curve):
707         pass
708
709     def _on_delete_curve(self, _class, method, playlist, curve):
710         os.remove(curve.path)
711
712     def _on_set_selected_playlist(self, _class, method, playlist):
713         """Call the `jump to playlist` command.
714         """
715         results = self.execute_command(
716             command=self._command_by_name('playlists'))
717         if not isinstance(results[-1], Success):
718             return
719         assert len(results) == 2, results
720         playlists = results[0]
721         matching = [p for p in playlists if p.name == playlist.name]
722         assert len(matching) == 1, matching
723         index = playlists.index(matching[0])
724         results = self.execute_command(
725             command=self._command_by_name('jump to playlist'),
726             args={'index':index})
727
728     def _on_set_selected_curve(self, _class, method, playlist, curve):
729         """Call the `jump to curve` command.
730         """
731         self._on_set_selected_playlist(_class, method, playlist)
732         index = playlist.index(curve)
733         results = self.execute_command(
734             command=self._command_by_name('jump to curve'),
735             args={'index':index})
736         if not isinstance(results[-1], Success):
737             return
738         #results = self.execute_command(
739         #    command=self._command_by_name('get playlist'))
740         #if not isinstance(results[-1], Success):
741         #    return
742         self.execute_command(
743             command=self._command_by_name('get curve'))
744
745
746
747     # Navbar interface
748
749     def _next_curve(self, *args):
750         """Call the `next curve` command.
751         """
752         results = self.execute_command(
753             command=self._command_by_name('next curve'))
754         if isinstance(results[-1], Success):
755             self.execute_command(
756                 command=self._command_by_name('get curve'))
757
758     def _previous_curve(self, *args):
759         """Call the `previous curve` command.
760         """
761         results = self.execute_command(
762             command=self._command_by_name('previous curve'))
763         if isinstance(results[-1], Success):
764             self.execute_command(
765                 command=self._command_by_name('get curve'))
766
767
768
769     # Panel display handling
770
771     def _on_panel_visibility(self, _class, method, panel_name, visible):
772         pane = self._c['manager'].GetPane(panel_name)
773         pane.Show(visible)
774         #if we don't do the following, the Folders pane does not resize properly on hide/show
775         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
776             #folders_size = pane.GetSize()
777             self.panelFolders.Fit()
778         self._c['manager'].Update()
779
780     def _setup_perspectives(self):
781         """Add perspectives to menubar and _perspectives.
782         """
783         self._perspectives = {
784             'Default': self._c['manager'].SavePerspective(),
785             }
786         path = self.gui.config['perspective path']
787         if os.path.isdir(path):
788             files = sorted(os.listdir(path))
789             for fname in files:
790                 name, extension = os.path.splitext(fname)
791                 if extension != self.gui.config['perspective extension']:
792                     continue
793                 fpath = os.path.join(path, fname)
794                 if not os.path.isfile(fpath):
795                     continue
796                 perspective = None
797                 with open(fpath, 'rU') as f:
798                     perspective = f.readline()
799                 if perspective:
800                     self._perspectives[name] = perspective
801
802         selected_perspective = self.gui.config['active perspective']
803         if not self._perspectives.has_key(selected_perspective):
804             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke
805
806         self._restore_perspective(selected_perspective, force=True)
807         self._update_perspective_menu()
808
809     def _update_perspective_menu(self):
810         self._c['menu bar']._c['perspective'].update(
811             sorted(self._perspectives.keys()),
812             self.gui.config['active perspective'])
813
814     def _save_perspective(self, perspective, perspective_dir, name,
815                           extension=None):
816         path = os.path.join(perspective_dir, name)
817         if extension != None:
818             path += extension
819         if not os.path.isdir(perspective_dir):
820             os.makedirs(perspective_dir)
821         with open(path, 'w') as f:
822             f.write(perspective)
823         self._perspectives[name] = perspective
824         self._restore_perspective(name)
825         self._update_perspective_menu()
826
827     def _delete_perspectives(self, perspective_dir, names,
828                              extension=None):
829         self.log.debug('remove perspectives %s from %s'
830                        % (names, perspective_dir))
831         for name in names:
832             path = os.path.join(perspective_dir, name)
833             if extension != None:
834                 path += extension
835             os.remove(path)
836             del(self._perspectives[name])
837         self._update_perspective_menu()
838         if self.gui.config['active perspective'] in names:
839             self._restore_perspective('Default')
840         # TODO: does this bug still apply?
841         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
842         #   http://trac.wxwidgets.org/ticket/3258 
843         # ) that makes the radio item indicator in the menu disappear.
844         # The code should be fine once this issue is fixed.
845
846     def _restore_perspective(self, name, force=False):
847         if name != self.gui.config['active perspective'] or force == True:
848             self.log.debug('restore perspective %s' % name)
849             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke
850             self._c['manager'].LoadPerspective(self._perspectives[name])
851             self._c['manager'].Update()
852             for pane in self._c['manager'].GetAllPanes():
853                 view = self._c['menu bar']._c['view']
854                 if pane.name in view._c.keys():
855                     view._c[pane.name].Check(pane.window.IsShown())
856
857     def _on_save_perspective(self, *args):
858         perspective = self._c['manager'].SavePerspective()
859         name = self.gui.config['active perspective']
860         if name == 'Default':
861             name = 'New perspective'
862         name = select_save_file(
863             directory=self.gui.config['perspective path'],
864             name=name,
865             extension=self.gui.config['perspective extension'],
866             parent=self,
867             message='Enter a name for the new perspective:',
868             caption='Save perspective')
869         if name == None:
870             return
871         self._save_perspective(
872             perspective, self.gui.config['perspective path'], name=name,
873             extension=self.gui.config['perspective extension'])
874
875     def _on_delete_perspective(self, *args, **kwargs):
876         options = sorted([p for p in self._perspectives.keys()
877                           if p != 'Default'])
878         dialog = SelectionDialog(
879             options=options,
880             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
881             button_id=wx.ID_DELETE,
882             selection_style='multiple',
883             parent=self,
884             title='Delete perspective(s)',
885             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
886         dialog.CenterOnScreen()
887         dialog.ShowModal()
888         names = [options[i] for i in dialog.selected]
889         dialog.Destroy()
890         self._delete_perspectives(
891             self.gui.config['perspective path'], names=names,
892             extension=self.gui.config['perspective extension'])
893
894     def _on_select_perspective(self, _class, method, name):
895         self._restore_perspective(name)
896
897
898
899 class HookeApp (wx.App):
900     """A :class:`wx.App` wrapper around :class:`HookeFrame`.
901
902     Tosses up a splash screen and then loads :class:`HookeFrame` in
903     its own window.
904     """
905     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
906         self.gui = gui
907         self.commands = commands
908         self.inqueue = inqueue
909         self.outqueue = outqueue
910         super(HookeApp, self).__init__(*args, **kwargs)
911
912     def OnInit(self):
913         self.SetAppName('Hooke')
914         self.SetVendorName('')
915         self._setup_splash_screen()
916
917         height = self.gui.config['main height']
918         width = self.gui.config['main width']
919         top = self.gui.config['main top']
920         left = self.gui.config['main left']
921
922         # Sometimes, the ini file gets confused and sets 'left' and
923         # 'top' to large negative numbers.  Here we catch and fix
924         # this.  Keep small negative numbers, the user might want
925         # those.
926         if left < -width:
927             left = 0
928         if top < -height:
929             top = 0
930
931         self._c = {
932             'frame': HookeFrame(
933                 self.gui, self.commands, self.inqueue, self.outqueue,
934                 parent=None, title='Hooke',
935                 pos=(left, top), size=(width, height),
936                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
937             }
938         self._c['frame'].Show(True)
939         self.SetTopWindow(self._c['frame'])
940         return True
941
942     def _setup_splash_screen(self):
943         if self.gui.config['show splash screen'] == True:
944             path = self.gui.config['splash screen image']
945             if os.path.isfile(path):
946                 duration = self.gui.config['splash screen duration']
947                 wx.SplashScreen(
948                     bitmap=wx.Image(path).ConvertToBitmap(),
949                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
950                     milliseconds=duration,
951                     parent=None)
952                 wx.Yield()
953                 # For some reason splashDuration and sleep do not
954                 # correspond to each other at least not on Windows.
955                 # Maybe it's because duration is in milliseconds and
956                 # sleep in seconds.  Thus we need to increase the
957                 # sleep time a bit. A factor of 1.2 seems to work.
958                 sleepFactor = 1.2
959                 time.sleep(sleepFactor * duration / 1000)
960
961
962 class GUI (UserInterface):
963     """wxWindows graphical user interface.
964     """
965     def __init__(self):
966         super(GUI, self).__init__(name='gui')
967
968     def default_settings(self):
969         """Return a list of :class:`hooke.config.Setting`\s for any
970         configurable UI settings.
971
972         The suggested section setting is::
973
974             Setting(section=self.setting_section, help=self.__doc__)
975         """
976         return [
977             Setting(section=self.setting_section, help=self.__doc__),
978             Setting(section=self.setting_section, option='icon image',
979                     value=os.path.join('doc', 'img', 'microscope.ico'),
980                     type='file',
981                     help='Path to the hooke icon image.'),
982             Setting(section=self.setting_section, option='show splash screen',
983                     value=True, type='bool',
984                     help='Enable/disable the splash screen'),
985             Setting(section=self.setting_section, option='splash screen image',
986                     value=os.path.join('doc', 'img', 'hooke.jpg'),
987                     type='file',
988                     help='Path to the Hooke splash screen image.'),
989             Setting(section=self.setting_section,
990                     option='splash screen duration',
991                     value=1000, type='int',
992                     help='Duration of the splash screen in milliseconds.'),
993             Setting(section=self.setting_section, option='perspective path',
994                     value=os.path.join('resources', 'gui', 'perspective'),
995                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
996             Setting(section=self.setting_section, option='perspective extension',
997                     value='.txt',
998                     help='Extension for perspective files.'),
999             Setting(section=self.setting_section, option='hide extensions',
1000                     value=False, type='bool',
1001                     help='Hide file extensions when displaying names.'),
1002             Setting(section=self.setting_section, option='plot legend',
1003                     value=True, type='bool',
1004                     help='Enable/disable the plot legend.'),
1005             Setting(section=self.setting_section, option='plot SI format',
1006                     value='True', type='bool',
1007                     help='Enable/disable SI plot axes numbering.'),
1008             Setting(section=self.setting_section, option='plot decimals',
1009                     value=2, type='int',
1010                     help='Number of decimal places to show if "plot SI format" is enabled.'),
1011             Setting(section=self.setting_section, option='folders-workdir',
1012                     value='.', type='path',
1013                     help='This should probably go...'),
1014             Setting(section=self.setting_section, option='folders-filters',
1015                     value='.', type='path',
1016                     help='This should probably go...'),
1017             Setting(section=self.setting_section, option='active perspective',
1018                     value='Default',
1019                     help='Name of active perspective file (or "Default").'),
1020             Setting(section=self.setting_section,
1021                     option='folders-filter-index',
1022                     value=0, type='int',
1023                     help='This should probably go...'),
1024             Setting(section=self.setting_section, option='main height',
1025                     value=450, type='int',
1026                     help='Height of main window in pixels.'),
1027             Setting(section=self.setting_section, option='main width',
1028                     value=800, type='int',
1029                     help='Width of main window in pixels.'),
1030             Setting(section=self.setting_section, option='main top',
1031                     value=0, type='int',
1032                     help='Pixels from screen top to top of main window.'),
1033             Setting(section=self.setting_section, option='main left',
1034                     value=0, type='int',
1035                     help='Pixels from screen left to left of main window.'),
1036             Setting(section=self.setting_section, option='selected command',
1037                     value='load playlist',
1038                     help='Name of the initially selected command.'),
1039             ]
1040
1041     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1042         redirect = True
1043         if __debug__:
1044             redirect=False
1045         app = HookeApp(gui=self,
1046                        commands=commands,
1047                        inqueue=ui_to_command_queue,
1048                        outqueue=command_to_ui_queue,
1049                        redirect=redirect)
1050         return app
1051
1052     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1053         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
1054         app.MainLoop()