Don't save non-string portions of *.info in playlist xml files
[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=int(self.gui.config['folders-filter-index'])), 'left'),  #HACK: config should convert
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':  # HACK: config should decode
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         # TODO: note list interface in NotePanel.
692         self.execute_command(
693             command=self._command_by_name('set note'),
694             args={'note':text})
695
696
697
698     # Playlist panel interface
699
700     def _on_user_delete_playlist(self, _class, method, playlist):
701         pass
702
703     def _on_delete_playlist(self, _class, method, playlist):
704         if hasattr(playlist, 'path') and playlist.path != None:
705             os.remove(playlist.path)
706
707     def _on_user_delete_curve(self, _class, method, playlist, curve):
708         pass
709
710     def _on_delete_curve(self, _class, method, playlist, curve):
711         os.remove(curve.path)
712
713     def _on_set_selected_playlist(self, _class, method, playlist):
714         """TODO: playlists plugin with `jump to playlist`.
715         """
716         pass
717
718     def _on_set_selected_curve(self, _class, method, playlist, curve):
719         """Call the `jump to curve` command.
720
721         TODO: playlists plugin.
722         """
723         # TODO: jump to playlist, get playlist
724         index = playlist.index(curve)
725         results = self.execute_command(
726             command=self._command_by_name('jump to curve'),
727             args={'index':index})
728         if not isinstance(results[-1], Success):
729             return
730         #results = self.execute_command(
731         #    command=self._command_by_name('get playlist'))
732         #if not isinstance(results[-1], Success):
733         #    return
734         self.execute_command(
735             command=self._command_by_name('get curve'))
736
737
738
739     # Navbar interface
740
741     def _next_curve(self, *args):
742         """Call the `next curve` command.
743         """
744         results = self.execute_command(
745             command=self._command_by_name('next curve'))
746         if isinstance(results[-1], Success):
747             self.execute_command(
748                 command=self._command_by_name('get curve'))
749
750     def _previous_curve(self, *args):
751         """Call the `previous curve` command.
752         """
753         results = self.execute_command(
754             command=self._command_by_name('previous curve'))
755         if isinstance(results[-1], Success):
756             self.execute_command(
757                 command=self._command_by_name('get curve'))
758
759
760
761     # Panel display handling
762
763     def _on_panel_visibility(self, _class, method, panel_name, visible):
764         pane = self._c['manager'].GetPane(panel_name)
765         pane.Show(visible)
766         #if we don't do the following, the Folders pane does not resize properly on hide/show
767         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
768             #folders_size = pane.GetSize()
769             self.panelFolders.Fit()
770         self._c['manager'].Update()
771
772     def _setup_perspectives(self):
773         """Add perspectives to menubar and _perspectives.
774         """
775         self._perspectives = {
776             'Default': self._c['manager'].SavePerspective(),
777             }
778         path = self.gui.config['perspective path']
779         if os.path.isdir(path):
780             files = sorted(os.listdir(path))
781             for fname in files:
782                 name, extension = os.path.splitext(fname)
783                 if extension != self.gui.config['perspective extension']:
784                     continue
785                 fpath = os.path.join(path, fname)
786                 if not os.path.isfile(fpath):
787                     continue
788                 perspective = None
789                 with open(fpath, 'rU') as f:
790                     perspective = f.readline()
791                 if perspective:
792                     self._perspectives[name] = perspective
793
794         selected_perspective = self.gui.config['active perspective']
795         if not self._perspectives.has_key(selected_perspective):
796             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke
797
798         self._restore_perspective(selected_perspective, force=True)
799         self._update_perspective_menu()
800
801     def _update_perspective_menu(self):
802         self._c['menu bar']._c['perspective'].update(
803             sorted(self._perspectives.keys()),
804             self.gui.config['active perspective'])
805
806     def _save_perspective(self, perspective, perspective_dir, name,
807                           extension=None):
808         path = os.path.join(perspective_dir, name)
809         if extension != None:
810             path += extension
811         if not os.path.isdir(perspective_dir):
812             os.makedirs(perspective_dir)
813         with open(path, 'w') as f:
814             f.write(perspective)
815         self._perspectives[name] = perspective
816         self._restore_perspective(name)
817         self._update_perspective_menu()
818
819     def _delete_perspectives(self, perspective_dir, names,
820                              extension=None):
821         self.log.debug('remove perspectives %s from %s'
822                        % (names, perspective_dir))
823         for name in names:
824             path = os.path.join(perspective_dir, name)
825             if extension != None:
826                 path += extension
827             os.remove(path)
828             del(self._perspectives[name])
829         self._update_perspective_menu()
830         if self.gui.config['active perspective'] in names:
831             self._restore_perspective('Default')
832         # TODO: does this bug still apply?
833         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
834         #   http://trac.wxwidgets.org/ticket/3258 
835         # ) that makes the radio item indicator in the menu disappear.
836         # The code should be fine once this issue is fixed.
837
838     def _restore_perspective(self, name, force=False):
839         if name != self.gui.config['active perspective'] or force == True:
840             self.log.debug('restore perspective %s' % name)
841             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke
842             self._c['manager'].LoadPerspective(self._perspectives[name])
843             self._c['manager'].Update()
844             for pane in self._c['manager'].GetAllPanes():
845                 view = self._c['menu bar']._c['view']
846                 if pane.name in view._c.keys():
847                     view._c[pane.name].Check(pane.window.IsShown())
848
849     def _on_save_perspective(self, *args):
850         perspective = self._c['manager'].SavePerspective()
851         name = self.gui.config['active perspective']
852         if name == 'Default':
853             name = 'New perspective'
854         name = select_save_file(
855             directory=self.gui.config['perspective path'],
856             name=name,
857             extension=self.gui.config['perspective extension'],
858             parent=self,
859             message='Enter a name for the new perspective:',
860             caption='Save perspective')
861         if name == None:
862             return
863         self._save_perspective(
864             perspective, self.gui.config['perspective path'], name=name,
865             extension=self.gui.config['perspective extension'])
866
867     def _on_delete_perspective(self, *args, **kwargs):
868         options = sorted([p for p in self._perspectives.keys()
869                           if p != 'Default'])
870         dialog = SelectionDialog(
871             options=options,
872             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
873             button_id=wx.ID_DELETE,
874             selection_style='multiple',
875             parent=self,
876             title='Delete perspective(s)',
877             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
878         dialog.CenterOnScreen()
879         dialog.ShowModal()
880         names = [options[i] for i in dialog.selected]
881         dialog.Destroy()
882         self._delete_perspectives(
883             self.gui.config['perspective path'], names=names,
884             extension=self.gui.config['perspective extension'])
885
886     def _on_select_perspective(self, _class, method, name):
887         self._restore_perspective(name)
888
889
890
891 class HookeApp (wx.App):
892     """A :class:`wx.App` wrapper around :class:`HookeFrame`.
893
894     Tosses up a splash screen and then loads :class:`HookeFrame` in
895     its own window.
896     """
897     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
898         self.gui = gui
899         self.commands = commands
900         self.inqueue = inqueue
901         self.outqueue = outqueue
902         super(HookeApp, self).__init__(*args, **kwargs)
903
904     def OnInit(self):
905         self.SetAppName('Hooke')
906         self.SetVendorName('')
907         self._setup_splash_screen()
908
909         height = int(self.gui.config['main height']) # HACK: config should convert
910         width = int(self.gui.config['main width'])
911         top = int(self.gui.config['main top'])
912         left = int(self.gui.config['main left'])
913
914         # Sometimes, the ini file gets confused and sets 'left' and
915         # 'top' to large negative numbers.  Here we catch and fix
916         # this.  Keep small negative numbers, the user might want
917         # those.
918         if left < -width:
919             left = 0
920         if top < -height:
921             top = 0
922
923         self._c = {
924             'frame': HookeFrame(
925                 self.gui, self.commands, self.inqueue, self.outqueue,
926                 parent=None, title='Hooke',
927                 pos=(left, top), size=(width, height),
928                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
929             }
930         self._c['frame'].Show(True)
931         self.SetTopWindow(self._c['frame'])
932         return True
933
934     def _setup_splash_screen(self):
935         if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
936             path = self.gui.config['splash screen image']
937             if os.path.isfile(path):
938                 duration = int(self.gui.config['splash screen duration'])  # HACK: config should decode types
939                 wx.SplashScreen(
940                     bitmap=wx.Image(path).ConvertToBitmap(),
941                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
942                     milliseconds=duration,
943                     parent=None)
944                 wx.Yield()
945                 # For some reason splashDuration and sleep do not
946                 # correspond to each other at least not on Windows.
947                 # Maybe it's because duration is in milliseconds and
948                 # sleep in seconds.  Thus we need to increase the
949                 # sleep time a bit. A factor of 1.2 seems to work.
950                 sleepFactor = 1.2
951                 time.sleep(sleepFactor * duration / 1000)
952
953
954 class GUI (UserInterface):
955     """wxWindows graphical user interface.
956     """
957     def __init__(self):
958         super(GUI, self).__init__(name='gui')
959
960     def default_settings(self):
961         """Return a list of :class:`hooke.config.Setting`\s for any
962         configurable UI settings.
963
964         The suggested section setting is::
965
966             Setting(section=self.setting_section, help=self.__doc__)
967         """
968         return [
969             Setting(section=self.setting_section, help=self.__doc__),
970             Setting(section=self.setting_section, option='icon image',
971                     value=os.path.join('doc', 'img', 'microscope.ico'),
972                     help='Path to the hooke icon image.'),
973             Setting(section=self.setting_section, option='show splash screen',
974                     value=True,
975                     help='Enable/disable the splash screen'),
976             Setting(section=self.setting_section, option='splash screen image',
977                     value=os.path.join('doc', 'img', 'hooke.jpg'),
978                     help='Path to the Hooke splash screen image.'),
979             Setting(section=self.setting_section, option='splash screen duration',
980                     value=1000,
981                     help='Duration of the splash screen in milliseconds.'),
982             Setting(section=self.setting_section, option='perspective path',
983                     value=os.path.join('resources', 'gui', 'perspective'),
984                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
985             Setting(section=self.setting_section, option='perspective extension',
986                     value='.txt',
987                     help='Extension for perspective files.'),
988             Setting(section=self.setting_section, option='hide extensions',
989                     value=False,
990                     help='Hide file extensions when displaying names.'),
991             Setting(section=self.setting_section, option='plot legend',
992                     value=True,
993                     help='Enable/disable the plot legend.'),
994             Setting(section=self.setting_section, option='plot SI format',
995                     value='True',
996                     help='Enable/disable SI plot axes numbering.'),
997             Setting(section=self.setting_section, option='plot decimals',
998                     value=2,
999                     help='Number of decimal places to show if "plot SI format" is enabled.'),
1000             Setting(section=self.setting_section, option='folders-workdir',
1001                     value='.',
1002                     help='This should probably go...'),
1003             Setting(section=self.setting_section, option='folders-filters',
1004                     value='.',
1005                     help='This should probably go...'),
1006             Setting(section=self.setting_section, option='active perspective',
1007                     value='Default',
1008                     help='Name of active perspective file (or "Default").'),
1009             Setting(section=self.setting_section, option='folders-filter-index',
1010                     value='0',
1011                     help='This should probably go...'),
1012             Setting(section=self.setting_section, option='main height',
1013                     value=450,
1014                     help='Height of main window in pixels.'),
1015             Setting(section=self.setting_section, option='main width',
1016                     value=800,
1017                     help='Width of main window in pixels.'),
1018             Setting(section=self.setting_section, option='main top',
1019                     value=0,
1020                     help='Pixels from screen top to top of main window.'),
1021             Setting(section=self.setting_section, option='main left',
1022                     value=0,
1023                     help='Pixels from screen left to left of main window.'),            
1024             Setting(section=self.setting_section, option='selected command',
1025                     value='load playlist',
1026                     help='Name of the initially selected command.'),
1027             ]
1028
1029     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1030         redirect = True
1031         if __debug__:
1032             redirect=False
1033         app = HookeApp(gui=self,
1034                        commands=commands,
1035                        inqueue=ui_to_command_queue,
1036                        outqueue=command_to_ui_queue,
1037                        redirect=redirect)
1038         return app
1039
1040     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1041         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
1042         app.MainLoop()