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