Ran update_copyright.py
[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             print sorted(curve.info.keys())
420             self._c['note'].set_text(curve.info['note'])
421         if 'playlist' in self._c:
422             self._c['playlist']._c['tree'].set_selected_curve(
423                 playlist, curve)
424         if 'plot' in self._c:
425             self._c['plot'].set_curve(curve, config=self.gui.config)
426
427     def _postprocess_next_curve(self, command, args={}, results=[]):
428         """No-op.  Only call 'next curve' via `self._next_curve()`.
429         """
430         pass
431
432     def _postprocess_previous_curve(self, command, args={}, results=[]):
433         """No-op.  Only call 'previous curve' via `self._previous_curve()`.
434         """
435         pass
436
437     def _postprocess_zero_block_surface_contact_point(
438         self, command, args={}, results=[]):
439         """Update the curve, since the available columns may have changed.
440         """
441         if isinstance(results[-1], Success):
442             self.execute_command(
443                 command=self._command_by_name('get curve'))
444  
445     def _postprocess_add_block_force_array(
446         self, command, args={}, results=[]):
447         """Update the curve, since the available columns may have changed.
448         """
449         if isinstance(results[-1], Success):
450             self.execute_command(
451                 command=self._command_by_name('get curve'))
452
453
454
455     # TODO: cruft
456
457     def _GetActiveFileIndex(self):
458         lib.playlist.Playlist = self.GetActivePlaylist()
459         #get the selected item from the tree
460         selected_item = self._c['playlist']._c['tree'].GetSelection()
461         #test if a playlist or a curve was double-clicked
462         if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
463             return -1
464         else:
465             count = 0
466             selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
467             while selected_item.IsOk():
468                 count += 1
469                 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
470             return count
471
472     def _GetPlaylistTab(self, name):
473         for index, page in enumerate(self._c['notebook']._tabs._pages):
474             if page.caption == name:
475                 return index
476         return -1
477
478     def select_plugin(self, _class=None, method=None, plugin=None):
479         pass
480
481     def AddPlaylistFromFiles(self, files=[], name='Untitled'):
482         if files:
483             playlist = lib.playlist.Playlist(self, self.drivers)
484             for item in files:
485                 playlist.add_curve(item)
486         if playlist.count > 0:
487             playlist.name = self._GetUniquePlaylistName(name)
488             playlist.reset()
489             self.AddTayliss(playlist)
490
491     def AppliesPlotmanipulator(self, name):
492         '''
493         Returns True if the plotmanipulator 'name' is applied, False otherwise
494         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
495         '''
496         return self.GetBoolFromConfig('core', 'plotmanipulators', name)
497
498     def ApplyPlotmanipulators(self, plot, plot_file):
499         '''
500         Apply all active plotmanipulators.
501         '''
502         if plot is not None and plot_file is not None:
503             manipulated_plot = copy.deepcopy(plot)
504             for plotmanipulator in self.plotmanipulators:
505                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
506                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
507             return manipulated_plot
508
509     def GetActiveFigure(self):
510         playlist_name = self.GetActivePlaylistName()
511         figure = self.playlists[playlist_name].figure
512         if figure is not None:
513             return figure
514         return None
515
516     def GetActiveFile(self):
517         playlist = self.GetActivePlaylist()
518         if playlist is not None:
519             return playlist.get_active_file()
520         return None
521
522     def GetActivePlot(self):
523         playlist = self.GetActivePlaylist()
524         if playlist is not None:
525             return playlist.get_active_file().plot
526         return None
527
528     def GetDisplayedPlot(self):
529         plot = copy.deepcopy(self.displayed_plot)
530         #plot.curves = []
531         #plot.curves = copy.deepcopy(plot.curves)
532         return plot
533
534     def GetDisplayedPlotCorrected(self):
535         plot = copy.deepcopy(self.displayed_plot)
536         plot.curves = []
537         plot.curves = copy.deepcopy(plot.corrected_curves)
538         return plot
539
540     def GetDisplayedPlotRaw(self):
541         plot = copy.deepcopy(self.displayed_plot)
542         plot.curves = []
543         plot.curves = copy.deepcopy(plot.raw_curves)
544         return plot
545
546     def GetDockArt(self):
547         return self._c['manager'].GetArtProvider()
548
549     def GetPlotmanipulator(self, name):
550         '''
551         Returns a plot manipulator function from its name
552         '''
553         for plotmanipulator in self.plotmanipulators:
554             if plotmanipulator.name == name:
555                 return plotmanipulator
556         return None
557
558     def HasPlotmanipulator(self, name):
559         '''
560         returns True if the plotmanipulator 'name' is loaded, False otherwise
561         '''
562         for plotmanipulator in self.plotmanipulators:
563             if plotmanipulator.command == name:
564                 return True
565         return False
566
567
568     def _on_dir_ctrl_left_double_click(self, event):
569         file_path = self.panelFolders.GetPath()
570         if os.path.isfile(file_path):
571             if file_path.endswith('.hkp'):
572                 self.do_loadlist(file_path)
573         event.Skip()
574
575     def _on_erase_background(self, event):
576         event.Skip()
577
578     def _on_notebook_page_close(self, event):
579         ctrl = event.GetEventObject()
580         playlist_name = ctrl.GetPageText(ctrl._curpage)
581         self.DeleteFromPlaylists(playlist_name)
582
583     def OnPaneClose(self, event):
584         event.Skip()
585
586     def OnPropGridChanged (self, event):
587         prop = event.GetProperty()
588         if prop:
589             item_section = self.panelProperties.SelectedTreeItem
590             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
591             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
592             config = self.gui.config[plugin]
593             property_section = self._c['commands']._c['tree'].GetItemText(item_section)
594             property_key = prop.GetName()
595             property_value = prop.GetDisplayedString()
596
597             config[property_section][property_key]['value'] = property_value
598
599     def OnResultsCheck(self, index, flag):
600         results = self.GetActivePlot().results
601         if results.has_key(self.results_str):
602             results[self.results_str].results[index].visible = flag
603             results[self.results_str].update()
604             self.UpdatePlot()
605
606
607     def _on_size(self, event):
608         event.Skip()
609
610     def UpdatePlaylistsTreeSelection(self):
611         playlist = self.GetActivePlaylist()
612         if playlist is not None:
613             if playlist.index >= 0:
614                 self._c['status bar'].set_playlist(playlist)
615                 self.UpdateNote()
616                 self.UpdatePlot()
617
618     def _on_curve_select(self, playlist, curve):
619         #create the plot tab and add playlist to the dictionary
620         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
621         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
622         #tab_index = self._c['notebook'].GetSelection()
623         playlist.figure = plotPanel.get_figure()
624         self.playlists[playlist.name] = playlist
625         #self.playlists[playlist.name] = [playlist, figure]
626         self._c['status bar'].set_playlist(playlist)
627         self.UpdateNote()
628         self.UpdatePlot()
629
630
631     def _on_playlist_left_doubleclick(self):
632         index = self._c['notebook'].GetSelection()
633         current_playlist = self._c['notebook'].GetPageText(index)
634         if current_playlist != playlist_name:
635             index = self._GetPlaylistTab(playlist_name)
636             self._c['notebook'].SetSelection(index)
637         self._c['status bar'].set_playlist(playlist)
638         self.UpdateNote()
639         self.UpdatePlot()
640
641     def _on_playlist_delete(self, playlist):
642         notebook = self.Parent.plotNotebook
643         index = self.Parent._GetPlaylistTab(playlist.name)
644         notebook.SetSelection(index)
645         notebook.DeletePage(notebook.GetSelection())
646         self.Parent.DeleteFromPlaylists(playlist_name)
647
648
649
650     # Command panel interface
651
652     def select_command(self, _class, method, command):
653         #self.select_plugin(plugin=command.plugin)
654         if 'assistant' in self._c:
655             self._c['assitant'].ChangeValue(command.help)
656         self._c['property editor'].clear()
657         for argument in command.arguments:
658             if argument.name == 'help':
659                 continue
660
661             results = self.execute_command(
662                 command=self._command_by_name('playlists'))
663             if not isinstance(results[-1], Success):
664                 self._postprocess_text(command, results=results)
665                 playlists = []
666             else:
667                 playlists = results[0]
668
669             results = self.execute_command(
670                 command=self._command_by_name('playlist curves'))
671             if not isinstance(results[-1], Success):
672                 self._postprocess_text(command, results=results)
673                 curves = []
674             else:
675                 curves = results[0]
676
677             p = prop_from_argument(
678                 argument, curves=curves, playlists=playlists)
679             if p == None:
680                 continue  # property intentionally not handled (yet)
681             self._c['property editor'].append_property(p)
682
683         self.gui.config['selected command'] = command  # TODO: push to engine
684
685
686
687     # Note panel interface
688
689     def _on_update_note(self, _class, method, text):
690         """Sets the note for the active curve.
691         """
692         # TODO: note list interface in NotePanel.
693         self.execute_command(
694             command=self._command_by_name('set note'),
695             args={'note':text})
696
697
698
699     # Playlist panel interface
700
701     def _on_user_delete_playlist(self, _class, method, playlist):
702         pass
703
704     def _on_delete_playlist(self, _class, method, playlist):
705         if hasattr(playlist, 'path') and playlist.path != None:
706             os.remove(playlist.path)
707
708     def _on_user_delete_curve(self, _class, method, playlist, curve):
709         pass
710
711     def _on_delete_curve(self, _class, method, playlist, curve):
712         os.remove(curve.path)
713
714     def _on_set_selected_playlist(self, _class, method, playlist):
715         """TODO: playlists plugin with `jump to playlist`.
716         """
717         pass
718
719     def _on_set_selected_curve(self, _class, method, playlist, curve):
720         """Call the `jump to curve` command.
721
722         TODO: playlists plugin.
723         """
724         # TODO: jump to playlist, get playlist
725         index = playlist.index(curve)
726         results = self.execute_command(
727             command=self._command_by_name('jump to curve'),
728             args={'index':index})
729         if not isinstance(results[-1], Success):
730             return
731         #results = self.execute_command(
732         #    command=self._command_by_name('get playlist'))
733         #if not isinstance(results[-1], Success):
734         #    return
735         self.execute_command(
736             command=self._command_by_name('get curve'))
737
738
739
740     # Navbar interface
741
742     def _next_curve(self, *args):
743         """Call the `next curve` command.
744         """
745         results = self.execute_command(
746             command=self._command_by_name('next curve'))
747         if isinstance(results[-1], Success):
748             self.execute_command(
749                 command=self._command_by_name('get curve'))
750
751     def _previous_curve(self, *args):
752         """Call the `previous curve` command.
753         """
754         results = self.execute_command(
755             command=self._command_by_name('previous curve'))
756         if isinstance(results[-1], Success):
757             self.execute_command(
758                 command=self._command_by_name('get curve'))
759
760
761
762     # Panel display handling
763
764     def _on_panel_visibility(self, _class, method, panel_name, visible):
765         pane = self._c['manager'].GetPane(panel_name)
766         pane.Show(visible)
767         #if we don't do the following, the Folders pane does not resize properly on hide/show
768         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
769             #folders_size = pane.GetSize()
770             self.panelFolders.Fit()
771         self._c['manager'].Update()
772
773     def _setup_perspectives(self):
774         """Add perspectives to menubar and _perspectives.
775         """
776         self._perspectives = {
777             'Default': self._c['manager'].SavePerspective(),
778             }
779         path = self.gui.config['perspective path']
780         if os.path.isdir(path):
781             files = sorted(os.listdir(path))
782             for fname in files:
783                 name, extension = os.path.splitext(fname)
784                 if extension != self.gui.config['perspective extension']:
785                     continue
786                 fpath = os.path.join(path, fname)
787                 if not os.path.isfile(fpath):
788                     continue
789                 perspective = None
790                 with open(fpath, 'rU') as f:
791                     perspective = f.readline()
792                 if perspective:
793                     self._perspectives[name] = perspective
794
795         selected_perspective = self.gui.config['active perspective']
796         if not self._perspectives.has_key(selected_perspective):
797             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke
798
799         self._restore_perspective(selected_perspective, force=True)
800         self._update_perspective_menu()
801
802     def _update_perspective_menu(self):
803         self._c['menu bar']._c['perspective'].update(
804             sorted(self._perspectives.keys()),
805             self.gui.config['active perspective'])
806
807     def _save_perspective(self, perspective, perspective_dir, name,
808                           extension=None):
809         path = os.path.join(perspective_dir, name)
810         if extension != None:
811             path += extension
812         if not os.path.isdir(perspective_dir):
813             os.makedirs(perspective_dir)
814         with open(path, 'w') as f:
815             f.write(perspective)
816         self._perspectives[name] = perspective
817         self._restore_perspective(name)
818         self._update_perspective_menu()
819
820     def _delete_perspectives(self, perspective_dir, names,
821                              extension=None):
822         self.log.debug('remove perspectives %s from %s'
823                        % (names, perspective_dir))
824         for name in names:
825             path = os.path.join(perspective_dir, name)
826             if extension != None:
827                 path += extension
828             os.remove(path)
829             del(self._perspectives[name])
830         self._update_perspective_menu()
831         if self.gui.config['active perspective'] in names:
832             self._restore_perspective('Default')
833         # TODO: does this bug still apply?
834         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
835         #   http://trac.wxwidgets.org/ticket/3258 
836         # ) that makes the radio item indicator in the menu disappear.
837         # The code should be fine once this issue is fixed.
838
839     def _restore_perspective(self, name, force=False):
840         if name != self.gui.config['active perspective'] or force == True:
841             self.log.debug('restore perspective %s' % name)
842             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke
843             self._c['manager'].LoadPerspective(self._perspectives[name])
844             self._c['manager'].Update()
845             for pane in self._c['manager'].GetAllPanes():
846                 view = self._c['menu bar']._c['view']
847                 if pane.name in view._c.keys():
848                     view._c[pane.name].Check(pane.window.IsShown())
849
850     def _on_save_perspective(self, *args):
851         perspective = self._c['manager'].SavePerspective()
852         name = self.gui.config['active perspective']
853         if name == 'Default':
854             name = 'New perspective'
855         name = select_save_file(
856             directory=self.gui.config['perspective path'],
857             name=name,
858             extension=self.gui.config['perspective extension'],
859             parent=self,
860             message='Enter a name for the new perspective:',
861             caption='Save perspective')
862         if name == None:
863             return
864         self._save_perspective(
865             perspective, self.gui.config['perspective path'], name=name,
866             extension=self.gui.config['perspective extension'])
867
868     def _on_delete_perspective(self, *args, **kwargs):
869         options = sorted([p for p in self._perspectives.keys()
870                           if p != 'Default'])
871         dialog = SelectionDialog(
872             options=options,
873             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
874             button_id=wx.ID_DELETE,
875             selection_style='multiple',
876             parent=self,
877             title='Delete perspective(s)',
878             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
879         dialog.CenterOnScreen()
880         dialog.ShowModal()
881         names = [options[i] for i in dialog.selected]
882         dialog.Destroy()
883         self._delete_perspectives(
884             self.gui.config['perspective path'], names=names,
885             extension=self.gui.config['perspective extension'])
886
887     def _on_select_perspective(self, _class, method, name):
888         self._restore_perspective(name)
889
890
891
892 class HookeApp (wx.App):
893     """A :class:`wx.App` wrapper around :class:`HookeFrame`.
894
895     Tosses up a splash screen and then loads :class:`HookeFrame` in
896     its own window.
897     """
898     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
899         self.gui = gui
900         self.commands = commands
901         self.inqueue = inqueue
902         self.outqueue = outqueue
903         super(HookeApp, self).__init__(*args, **kwargs)
904
905     def OnInit(self):
906         self.SetAppName('Hooke')
907         self.SetVendorName('')
908         self._setup_splash_screen()
909
910         height = int(self.gui.config['main height']) # HACK: config should convert
911         width = int(self.gui.config['main width'])
912         top = int(self.gui.config['main top'])
913         left = int(self.gui.config['main left'])
914
915         # Sometimes, the ini file gets confused and sets 'left' and
916         # 'top' to large negative numbers.  Here we catch and fix
917         # this.  Keep small negative numbers, the user might want
918         # those.
919         if left < -width:
920             left = 0
921         if top < -height:
922             top = 0
923
924         self._c = {
925             'frame': HookeFrame(
926                 self.gui, self.commands, self.inqueue, self.outqueue,
927                 parent=None, title='Hooke',
928                 pos=(left, top), size=(width, height),
929                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
930             }
931         self._c['frame'].Show(True)
932         self.SetTopWindow(self._c['frame'])
933         return True
934
935     def _setup_splash_screen(self):
936         if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
937             path = self.gui.config['splash screen image']
938             if os.path.isfile(path):
939                 duration = int(self.gui.config['splash screen duration'])  # HACK: config should decode types
940                 wx.SplashScreen(
941                     bitmap=wx.Image(path).ConvertToBitmap(),
942                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
943                     milliseconds=duration,
944                     parent=None)
945                 wx.Yield()
946                 # For some reason splashDuration and sleep do not
947                 # correspond to each other at least not on Windows.
948                 # Maybe it's because duration is in milliseconds and
949                 # sleep in seconds.  Thus we need to increase the
950                 # sleep time a bit. A factor of 1.2 seems to work.
951                 sleepFactor = 1.2
952                 time.sleep(sleepFactor * duration / 1000)
953
954
955 class GUI (UserInterface):
956     """wxWindows graphical user interface.
957     """
958     def __init__(self):
959         super(GUI, self).__init__(name='gui')
960
961     def default_settings(self):
962         """Return a list of :class:`hooke.config.Setting`\s for any
963         configurable UI settings.
964
965         The suggested section setting is::
966
967             Setting(section=self.setting_section, help=self.__doc__)
968         """
969         return [
970             Setting(section=self.setting_section, help=self.__doc__),
971             Setting(section=self.setting_section, option='icon image',
972                     value=os.path.join('doc', 'img', 'microscope.ico'),
973                     help='Path to the hooke icon image.'),
974             Setting(section=self.setting_section, option='show splash screen',
975                     value=True,
976                     help='Enable/disable the splash screen'),
977             Setting(section=self.setting_section, option='splash screen image',
978                     value=os.path.join('doc', 'img', 'hooke.jpg'),
979                     help='Path to the Hooke splash screen image.'),
980             Setting(section=self.setting_section, option='splash screen duration',
981                     value=1000,
982                     help='Duration of the splash screen in milliseconds.'),
983             Setting(section=self.setting_section, option='perspective path',
984                     value=os.path.join('resources', 'gui', 'perspective'),
985                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
986             Setting(section=self.setting_section, option='perspective extension',
987                     value='.txt',
988                     help='Extension for perspective files.'),
989             Setting(section=self.setting_section, option='hide extensions',
990                     value=False,
991                     help='Hide file extensions when displaying names.'),
992             Setting(section=self.setting_section, option='plot legend',
993                     value=True,
994                     help='Enable/disable the plot legend.'),
995             Setting(section=self.setting_section, option='plot SI format',
996                     value='True',
997                     help='Enable/disable SI plot axes numbering.'),
998             Setting(section=self.setting_section, option='plot decimals',
999                     value=2,
1000                     help='Number of decimal places to show if "plot SI format" is enabled.'),
1001             Setting(section=self.setting_section, option='folders-workdir',
1002                     value='.',
1003                     help='This should probably go...'),
1004             Setting(section=self.setting_section, option='folders-filters',
1005                     value='.',
1006                     help='This should probably go...'),
1007             Setting(section=self.setting_section, option='active perspective',
1008                     value='Default',
1009                     help='Name of active perspective file (or "Default").'),
1010             Setting(section=self.setting_section, option='folders-filter-index',
1011                     value='0',
1012                     help='This should probably go...'),
1013             Setting(section=self.setting_section, option='main height',
1014                     value=450,
1015                     help='Height of main window in pixels.'),
1016             Setting(section=self.setting_section, option='main width',
1017                     value=800,
1018                     help='Width of main window in pixels.'),
1019             Setting(section=self.setting_section, option='main top',
1020                     value=0,
1021                     help='Pixels from screen top to top of main window.'),
1022             Setting(section=self.setting_section, option='main left',
1023                     value=0,
1024                     help='Pixels from screen left to left of main window.'),            
1025             Setting(section=self.setting_section, option='selected command',
1026                     value='load playlist',
1027                     help='Name of the initially selected command.'),
1028             ]
1029
1030     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1031         redirect = True
1032         if __debug__:
1033             redirect=False
1034         app = HookeApp(gui=self,
1035                        commands=commands,
1036                        inqueue=ui_to_command_queue,
1037                        outqueue=command_to_ui_queue,
1038                        redirect=redirect)
1039         return app
1040
1041     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1042         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
1043         app.MainLoop()