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