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