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