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