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