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