ae1a9a9ed22e5946813a3d377e92553eac955761
[hooke.git] / hooke / ui / gui / interface.py
1 # Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """Define :class:`HookeApp` and related, central application classes.
20 """
21
22 WX_GOOD=['2.9']
23
24 import wxversion
25 wxversion.select(WX_GOOD)
26
27 import copy
28 import logging
29 import os
30 import os.path
31 import platform
32 import shutil
33 import time
34
35 import wx.html
36 import wx.aui as aui
37 import wx.lib.evtmgr as evtmgr
38 # wxPropertyGrid is included in wxPython >= 2.9.1, see
39 #   http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
40 # until then, we'll avoid it because of the *nix build problems.
41 #import wx.propgrid as wxpg
42
43 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
44 from ...engine import CommandMessage
45 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
46 from .dialog.selection import Selection as SelectionDialog
47 from .dialog.save_file import select_save_file
48 from . import menu as menu
49 from . import navbar as navbar
50 from . import panel as panel
51 from .panel.propertyeditor import props_from_argument, props_from_setting
52 from . import statusbar as statusbar
53
54
55 class HookeFrame (wx.Frame):
56     """The main Hooke-interface window.    
57     """
58     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
59         super(HookeFrame, self).__init__(*args, **kwargs)
60         self.log = logging.getLogger('hooke')
61         self.gui = gui
62         self.commands = commands
63         self.inqueue = inqueue
64         self.outqueue = outqueue
65         self._perspectives = {}  # {name: perspective_str}
66         self._c = {}
67
68         self.SetIcon(wx.Icon(
69                 os.path.expanduser(self.gui.config['icon image']),
70                 wx.BITMAP_TYPE_ICO))
71
72         # setup frame manager
73         self._c['manager'] = aui.AuiManager()
74         self._c['manager'].SetManagedWindow(self)
75
76         # set the gradient and drag styles
77         self._c['manager'].GetArtProvider().SetMetric(
78             aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
79         self._c['manager'].SetFlags(
80             self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
81
82         # Min size for the frame itself isn't completely done.  See
83         # the end of FrameManager::Update() for the test code. For
84         # now, just hard code a frame minimum size.
85         #self.SetMinSize(wx.Size(500, 500))
86
87         self._setup_panels()
88         self._setup_toolbars()
89         self._c['manager'].Update()  # commit pending changes
90
91         # Create the menubar after the panes so that the default
92         # perspective is created with all panes open
93         panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
94         self._c['menu bar'] = menu.HookeMenuBar(
95             parent=self,
96             panels=panels,
97             callbacks={
98                 'close': self._on_close,
99                 'about': self._on_about,
100                 'view_panel': self._on_panel_visibility,
101                 'save_perspective': self._on_save_perspective,
102                 'delete_perspective': self._on_delete_perspective,
103                 'select_perspective': self._on_select_perspective,
104                 })
105         self.SetMenuBar(self._c['menu bar'])
106
107         self._c['status bar'] = statusbar.StatusBar(
108             parent=self,
109             style=wx.ST_SIZEGRIP)
110         self.SetStatusBar(self._c['status bar'])
111
112         self._setup_perspectives()
113         self._bind_events()
114         return # TODO: cleanup
115         self._displayed_plot = None
116         #load default list, if possible
117         self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
118
119
120     # GUI maintenance
121
122     def _setup_panels(self):
123         client_size = self.GetClientSize()
124         for p,style in [
125 #            ('folders', wx.GenericDirCtrl(
126 #                    parent=self,
127 #                    dir=self.gui.config['folders-workdir'],
128 #                    size=(200, 250),
129 #                    style=wx.DIRCTRL_SHOW_FILTERS,
130 #                    filter=self.gui.config['folders-filters'],
131 #                    defaultFilter=self.gui.config['folders-filter-index']), 'left'),
132             (panel.PANELS['playlist'](
133                     callbacks={
134                         'delete_playlist':self._on_user_delete_playlist,
135                         '_delete_playlist':self._on_delete_playlist,
136                         'delete_curve':self._on_user_delete_curve,
137                         '_delete_curve':self._on_delete_curve,
138                         '_on_set_selected_playlist':self._on_set_selected_playlist,
139                         '_on_set_selected_curve':self._on_set_selected_curve,
140                         },
141                     parent=self,
142                     style=wx.WANTS_CHARS|wx.NO_BORDER,
143                     # WANTS_CHARS so the panel doesn't eat the Return key.
144 #                    size=(160, 200),
145                     ), 'left'),
146             (panel.PANELS['note'](
147                     callbacks = {
148                         '_on_update':self._on_update_note,
149                         },
150                     parent=self,
151                     style=wx.WANTS_CHARS|wx.NO_BORDER,
152 #                    size=(160, 200),
153                     ), 'left'),
154 #            ('notebook', Notebook(
155 #                    parent=self,
156 #                    pos=wx.Point(client_size.x, client_size.y),
157 #                    size=wx.Size(430, 200),
158 #                    style=aui.AUI_NB_DEFAULT_STYLE
159 #                    | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
160             (panel.PANELS['commands'](
161                     commands=self.commands,
162                     selected=self.gui.config['selected command'],
163                     callbacks={
164                         'execute': self.explicit_execute_command,
165                         'select_plugin': self.select_plugin,
166                         'select_command': self.select_command,
167 #                        'selection_changed': self.panelProperties.select(self, method, command),  #SelectedTreeItem = selected_item,
168                         },
169                     parent=self,
170                     style=wx.WANTS_CHARS|wx.NO_BORDER,
171                     # WANTS_CHARS so the panel doesn't eat the Return key.
172 #                    size=(160, 200),
173                     ), 'right'),
174             (panel.PANELS['propertyeditor'](
175                     callbacks={},
176                     parent=self,
177                     style=wx.WANTS_CHARS,
178                     # WANTS_CHARS so the panel doesn't eat the Return key.
179                     ), 'center'),
180             (panel.PANELS['plot'](
181                     callbacks={
182                         '_set_status_text': self._on_plot_status_text,
183                         },
184                     parent=self,
185                     style=wx.WANTS_CHARS|wx.NO_BORDER,
186                     # WANTS_CHARS so the panel doesn't eat the Return key.
187 #                    size=(160, 200),
188                     ), 'center'),
189             (panel.PANELS['output'](
190                     parent=self,
191                     pos=wx.Point(0, 0),
192                     size=wx.Size(150, 90),
193                     style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
194              'bottom'),
195             ]:
196             self._add_panel(p, style)
197         self.execute_command(  # setup already loaded playlists
198             command=self._command_by_name('playlists'))
199         self.execute_command(  # setup already loaded curve
200             command=self._command_by_name('get curve'))
201
202     def _add_panel(self, panel, style):
203         self._c[panel.name] = panel
204         m_name = panel.managed_name
205         info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
206         info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
207         if style == 'top':
208             info.Top()
209         elif style == 'center':
210             info.CenterPane()
211         elif style == 'left':
212             info.Left()
213         elif style == 'right':
214             info.Right()
215         else:
216             assert style == 'bottom', style
217             info.Bottom()
218         self._c['manager'].AddPane(panel, info)
219
220     def _setup_toolbars(self):
221         self._c['navigation bar'] = navbar.NavBar(
222             callbacks={
223                 'next': self._next_curve,
224                 'previous': self._previous_curve,
225                 },
226             parent=self,
227             style=wx.TB_FLAT | wx.TB_NODIVIDER)
228         self._c['manager'].AddPane(
229             self._c['navigation bar'],
230             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
231                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
232                 ).RightDockable(False))
233
234     def _bind_events(self):
235         # TODO: figure out if we can use the eventManager for menu
236         # ranges and events of 'self' without raising an assertion
237         # fail error.
238         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
239         self.Bind(wx.EVT_SIZE, self._on_size)
240         self.Bind(wx.EVT_CLOSE, self._on_close)
241         self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
242
243         return # TODO: cleanup
244         treeCtrl = self._c['folders'].GetTreeCtrl()
245         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
246
247     def _on_about(self, *args):
248         dialog = wx.MessageDialog(
249             parent=self,
250             message=self.gui._splash_text(extra_info={
251                     'get-details':'click "Help -> License"'},
252                                           wrap=False),
253             caption='About Hooke',
254             style=wx.OK|wx.ICON_INFORMATION)
255         dialog.ShowModal()
256         dialog.Destroy()
257
258     def _on_size(self, event):
259         event.Skip()
260
261     def _on_close(self, *args):
262         self.log.info('closing GUI framework')
263         # apply changes
264         self._set_config('main height', self.GetSize().GetHeight())
265         self._set_config('main left', self.GetPosition()[0])
266         self._set_config('main top', self.GetPosition()[1])
267         self._set_config('main width', self.GetSize().GetWidth())
268         self._c['manager'].UnInit()
269         del self._c['manager']
270         self.Destroy()
271
272     def _on_erase_background(self, event):
273         event.Skip()
274
275
276
277     # Panel utility functions
278
279     def _file_name(self, name):
280         """Cleanup names according to configured preferences.
281         """
282         if self.gui.config['hide extensions'] == True:
283             name,ext = os.path.splitext(name)
284         return name
285
286
287
288     # Command handling
289
290     def _command_by_name(self, name):
291         cs = [c for c in self.commands if c.name == name]
292         if len(cs) == 0:
293             raise KeyError(name)
294         elif len(cs) > 1:
295             raise Exception('Multiple commands named "%s"' % name)
296         return cs[0]
297
298     def explicit_execute_command(self, _class=None, method=None,
299                                  command=None, args=None):
300         return self.execute_command(
301             _class=_class, method=method, command=command, args=args,
302             explicit_user_call=True)
303
304     def execute_command(self, _class=None, method=None,
305                         command=None, args=None, explicit_user_call=False):
306         if args == None:
307             args = {}
308         if ('property editor' in self._c
309             and self.gui.config['selected command'] == command.name):
310             for name,value in self._c['property editor'].get_values().items():
311                 arg = self._c['property editor']._argument_from_label.get(
312                     name, None)
313                 if arg == None:
314                     continue
315                 elif arg.count == 1:
316                     args[arg.name] = value
317                     continue
318                 # deal with counted arguments
319                 if arg.name not in args:
320                     args[arg.name] = {}
321                 index = int(name[len(arg.name):])
322                 args[arg.name][index] = value
323             for arg in command.arguments:
324                 if arg.name not in args:
325                     continue  # undisplayed argument, e.g. 'driver' types.
326                 count = arg.count
327                 if hasattr(arg, '_display_count'):  # support HACK in props_from_argument()
328                     count = arg._display_count
329                 if count != 1 and arg.name in args:
330                     keys = sorted(args[arg.name].keys())
331                     assert keys == range(count), keys
332                     args[arg.name] = [args[arg.name][i]
333                                       for i in range(count)]
334                 if arg.count == -1:
335                     while (len(args[arg.name]) > 0
336                            and args[arg.name][-1] == None):
337                         args[arg.name].pop()
338                     if len(args[arg.name]) == 0:
339                         args[arg.name] = arg.default
340         cm = CommandMessage(command.name, args)
341         self.gui._submit_command(
342             cm, self.inqueue, explicit_user_call=explicit_user_call)
343         # TODO: skip responses for commands that were captured by the
344         # command stack.  We'd need to poll on each request, remember
345         # capture state, or add a flag to the response...
346         return self._handle_response(command_message=cm)
347
348     def _handle_response(self, command_message):
349         results = []
350         while True:
351             msg = self.outqueue.get()
352             results.append(msg)
353             if isinstance(msg, Exit):
354                 self._on_close()
355                 break
356             elif isinstance(msg, CommandExit):
357                 # TODO: display command complete
358                 break
359             elif isinstance(msg, ReloadUserInterfaceConfig):
360                 self.gui.reload_config(msg.config)
361                 continue
362             elif isinstance(msg, Request):
363                 h = handler.HANDLERS[msg.type]
364                 h.run(self, msg)  # TODO: pause for response?
365                 continue
366         pp = getattr(
367            self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
368            self._postprocess_text)
369         pp(command=command_message.command,
370            args=command_message.arguments,
371            results=results)
372         return results
373
374     def _handle_request(self, msg):
375         """Repeatedly try to get a response to `msg`.
376         """
377         if prompt == None:
378             raise NotImplementedError('_%s_request_prompt' % msg.type)
379         prompt_string = prompt(msg)
380         parser = getattr(self, '_%s_request_parser' % msg.type, None)
381         if parser == None:
382             raise NotImplementedError('_%s_request_parser' % msg.type)
383         error = None
384         while True:
385             if error != None:
386                 self.cmd.stdout.write(''.join([
387                         error.__class__.__name__, ': ', str(error), '\n']))
388             self.cmd.stdout.write(prompt_string)
389             value = parser(msg, self.cmd.stdin.readline())
390             try:
391                 response = msg.response(value)
392                 break
393             except ValueError, error:
394                 continue
395         self.inqueue.put(response)
396
397     def _set_config(self, option, value, section=None):
398         self.gui._set_config(section=section, option=option, value=value,
399                              ui_to_command_queue=self.inqueue,
400                              response_handler=self._handle_response)
401
402
403     # Command-specific postprocessing
404
405     def _postprocess_text(self, command, args={}, results=[]):
406         """Print the string representation of the results to the Results window.
407
408         This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
409         approach, except that :class:`~hooke.ui.commandline.DoCommand`
410         doesn't print some internally handled messages
411         (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
412         """
413         for result in results:
414             if isinstance(result, CommandExit):
415                 self._c['output'].write(result.__class__.__name__+'\n')
416             self._c['output'].write(str(result).rstrip()+'\n')
417
418     def _postprocess_playlists(self, command, args={}, results=None):
419         """Update `self` to show the playlists.
420         """
421         if not isinstance(results[-1], Success):
422             self._postprocess_text(command, results=results)
423             return
424         assert len(results) == 2, results
425         playlists = results[0]
426         if 'playlist' in self._c:
427             for playlist in playlists:
428                 if self._c['playlist'].is_playlist_loaded(playlist):
429                     self._c['playlist'].update_playlist(playlist)
430                 else:
431                     self._c['playlist'].add_playlist(playlist)
432
433     def _postprocess_new_playlist(self, command, args={}, results=None):
434         """Update `self` to show the new playlist.
435         """
436         if not isinstance(results[-1], Success):
437             self._postprocess_text(command, results=results)
438             return
439         assert len(results) == 2, results
440         playlist = results[0]
441         if 'playlist' in self._c:
442             loaded = self._c['playlist'].is_playlist_loaded(playlist)
443             assert loaded == False, loaded
444             self._c['playlist'].add_playlist(playlist)
445
446     def _postprocess_load_playlist(self, command, args={}, results=None):
447         """Update `self` to show the playlist.
448         """
449         if not isinstance(results[-1], Success):
450             self._postprocess_text(command, results=results)
451             return
452         assert len(results) == 2, results
453         playlist = results[0]
454         self._c['playlist'].add_playlist(playlist)
455
456     def _postprocess_get_playlist(self, command, args={}, results=[]):
457         if not isinstance(results[-1], Success):
458             self._postprocess_text(command, results=results)
459             return
460         assert len(results) == 2, results
461         playlist = results[0]
462         if 'playlist' in self._c:
463             loaded = self._c['playlist'].is_playlist_loaded(playlist)
464             assert loaded == True, loaded
465             self._c['playlist'].update_playlist(playlist)
466
467     def _postprocess_name_playlist(self, command, args={}, results=None):
468         """Update `self` to show the new playlist.
469         """
470         return self._postprocess_new_playlist(command, args, results)
471
472     def _postprocess_get_curve(self, command, args={}, results=[]):
473         """Update `self` to show the curve.
474         """
475         if not isinstance(results[-1], Success):
476             self._postprocess_text(command, results=results)
477             return
478         assert len(results) == 2, results
479         curve = results[0]
480         if args.get('curve', None) == None:
481             # the command defaults to the current curve of the current playlist
482             results = self.execute_command(
483                 command=self._command_by_name('get playlist'))
484             playlist = results[0]
485         else:
486             raise NotImplementedError()
487         if 'note' in self._c:
488             self._c['note'].set_text(curve.info.get('note', ''))
489         if 'playlist' in self._c:
490             self._c['playlist'].set_selected_curve(
491                 playlist, curve)
492         if 'plot' in self._c:
493             self._c['plot'].set_curve(curve, config=self.gui.config)
494
495     def _postprocess_next_curve(self, command, args={}, results=[]):
496         """No-op.  Only call 'next curve' via `self._next_curve()`.
497         """
498         pass
499
500     def _postprocess_previous_curve(self, command, args={}, results=[]):
501         """No-op.  Only call 'previous curve' via `self._previous_curve()`.
502         """
503         pass
504
505     def _postprocess_glob_curves_to_playlist(
506         self, command, args={}, results=[]):
507         """Update `self` to show new curves.
508         """
509         if not isinstance(results[-1], Success):
510             self._postprocess_text(command, results=results)
511             return
512         if 'playlist' in self._c:
513             if args.get('playlist', None) != None:
514                 playlist = args['playlist']
515                 pname = playlist.name
516                 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
517                 assert loaded == True, loaded
518                 for curve in results[:-1]:
519                     self._c['playlist']._add_curve(pname, curve)
520             else:
521                 self.execute_command(
522                     command=self._command_by_name('get playlist'))
523
524     def _update_curve(self, command, args={}, results=[]):
525         """Update the curve, since the available columns may have changed.
526         """
527         if isinstance(results[-1], Success):
528             self.execute_command(
529                 command=self._command_by_name('get curve'))
530
531
532     # Command panel interface
533
534     def select_command(self, _class, method, command):
535         #self.select_plugin(plugin=command.plugin)
536         self._c['property editor'].clear()
537         self._c['property editor']._argument_from_label = {}
538         for argument in command.arguments:
539             if argument.name == 'help':
540                 continue
541
542             results = self.execute_command(
543                 command=self._command_by_name('playlists'))
544             if not isinstance(results[-1], Success):
545                 self._postprocess_text(command, results=results)
546                 playlists = []
547             else:
548                 playlists = results[0]
549
550             results = self.execute_command(
551                 command=self._command_by_name('playlist curves'))
552             if not isinstance(results[-1], Success):
553                 self._postprocess_text(command, results=results)
554                 curves = []
555             else:
556                 curves = results[0]
557
558             ret = props_from_argument(
559                 argument, curves=curves, playlists=playlists)
560             if ret == None:
561                 continue  # property intentionally not handled (yet)
562             for label,p in ret:
563                 self._c['property editor'].append_property(p)
564                 self._c['property editor']._argument_from_label[label] = (
565                     argument)
566
567         self._set_config('selected command', command.name)
568
569     def select_plugin(self, _class=None, method=None, plugin=None):
570         pass
571
572
573
574     # Folders panel interface
575
576     def _on_dir_ctrl_left_double_click(self, event):
577         file_path = self.panelFolders.GetPath()
578         if os.path.isfile(file_path):
579             if file_path.endswith('.hkp'):
580                 self.do_loadlist(file_path)
581         event.Skip()
582
583
584
585     # Note panel interface
586
587     def _on_update_note(self, _class, method, text):
588         """Sets the note for the active curve.
589         """
590         self.execute_command(
591             command=self._command_by_name('set note'),
592             args={'note':text})
593
594
595
596     # Playlist panel interface
597
598     def _on_user_delete_playlist(self, _class, method, playlist):
599         pass
600
601     def _on_delete_playlist(self, _class, method, playlist):
602         if hasattr(playlist, 'path') and playlist.path != None:
603             os.remove(playlist.path)
604
605     def _on_user_delete_curve(self, _class, method, playlist, curve):
606         pass
607
608     def _on_delete_curve(self, _class, method, playlist, curve):
609         index = playlist.index(curve)
610         results = self.execute_command(
611             command=self._command_by_name('remove curve from playlist'),
612             args={'index': index})
613         #os.remove(curve.path)
614         pass
615
616     def _on_set_selected_playlist(self, _class, method, playlist):
617         """Call the `jump to playlist` command.
618         """
619         results = self.execute_command(
620             command=self._command_by_name('playlists'))
621         if not isinstance(results[-1], Success):
622             return
623         assert len(results) == 2, results
624         playlists = results[0]
625         matching = [p for p in playlists if p.name == playlist.name]
626         assert len(matching) == 1, matching
627         index = playlists.index(matching[0])
628         results = self.execute_command(
629             command=self._command_by_name('jump to playlist'),
630             args={'index':index})
631
632     def _on_set_selected_curve(self, _class, method, playlist, curve):
633         """Call the `jump to curve` command.
634         """
635         self._on_set_selected_playlist(_class, method, playlist)
636         index = playlist.index(curve)
637         results = self.execute_command(
638             command=self._command_by_name('jump to curve'),
639             args={'index':index})
640         if not isinstance(results[-1], Success):
641             return
642         #results = self.execute_command(
643         #    command=self._command_by_name('get playlist'))
644         #if not isinstance(results[-1], Success):
645         #    return
646         self.execute_command(
647             command=self._command_by_name('get curve'))
648
649
650
651     # Plot panel interface
652
653     def _on_plot_status_text(self, _class, method, text):
654         if 'status bar' in self._c:
655             self._c['status bar'].set_plot_text(text)
656
657
658
659     # Navbar interface
660
661     def _next_curve(self, *args):
662         """Call the `next curve` command.
663         """
664         results = self.execute_command(
665             command=self._command_by_name('next curve'))
666         if isinstance(results[-1], Success):
667             self.execute_command(
668                 command=self._command_by_name('get curve'))
669
670     def _previous_curve(self, *args):
671         """Call the `previous curve` command.
672         """
673         results = self.execute_command(
674             command=self._command_by_name('previous curve'))
675         if isinstance(results[-1], Success):
676             self.execute_command(
677                 command=self._command_by_name('get curve'))
678
679
680
681     # Panel display handling
682
683     def _on_pane_close(self, event):
684         pane = event.pane
685         view = self._c['menu bar']._c['view']
686         if pane.name in  view._c.keys():
687             view._c[pane.name].Check(False)
688         event.Skip()
689
690     def _on_panel_visibility(self, _class, method, panel_name, visible):
691         pane = self._c['manager'].GetPane(panel_name)
692         pane.Show(visible)
693         #if we don't do the following, the Folders pane does not resize properly on hide/show
694         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
695             #folders_size = pane.GetSize()
696             self.panelFolders.Fit()
697         self._c['manager'].Update()
698
699     def _setup_perspectives(self):
700         """Add perspectives to menubar and _perspectives.
701         """
702         self._perspectives = {
703             'Default': self._c['manager'].SavePerspective(),
704             }
705         path = os.path.expanduser(self.gui.config['perspective path'])
706         if os.path.isdir(path):
707             files = sorted(os.listdir(path))
708             for fname in files:
709                 name, extension = os.path.splitext(fname)
710                 if extension != self.gui.config['perspective extension']:
711                     continue
712                 fpath = os.path.join(path, fname)
713                 if not os.path.isfile(fpath):
714                     continue
715                 perspective = None
716                 with open(fpath, 'rU') as f:
717                     perspective = f.readline()
718                 if perspective:
719                     self._perspectives[name] = perspective
720
721         selected_perspective = self.gui.config['active perspective']
722         if not self._perspectives.has_key(selected_perspective):
723             self._set_config('active perspective', 'Default')
724
725         self._restore_perspective(selected_perspective, force=True)
726         self._update_perspective_menu()
727
728     def _update_perspective_menu(self):
729         self._c['menu bar']._c['perspective'].update(
730             sorted(self._perspectives.keys()),
731             self.gui.config['active perspective'])
732
733     def _save_perspective(self, perspective, perspective_dir, name,
734                           extension=None):
735         path = os.path.join(perspective_dir, name)
736         if extension != None:
737             path += extension
738         if not os.path.isdir(perspective_dir):
739             os.makedirs(perspective_dir)
740         with open(path, 'w') as f:
741             f.write(perspective)
742         self._perspectives[name] = perspective
743         self._restore_perspective(name)
744         self._update_perspective_menu()
745
746     def _delete_perspectives(self, perspective_dir, names,
747                              extension=None):
748         self.log.debug('remove perspectives %s from %s'
749                        % (names, perspective_dir))
750         for name in names:
751             path = os.path.join(perspective_dir, name)
752             if extension != None:
753                 path += extension
754             os.remove(path)
755             del(self._perspectives[name])
756         self._update_perspective_menu()
757         if self.gui.config['active perspective'] in names:
758             self._restore_perspective('Default')
759         # TODO: does this bug still apply?
760         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
761         #   http://trac.wxwidgets.org/ticket/3258 
762         # ) that makes the radio item indicator in the menu disappear.
763         # The code should be fine once this issue is fixed.
764
765     def _restore_perspective(self, name, force=False):
766         if name != self.gui.config['active perspective'] or force == True:
767             self.log.debug('restore perspective %s' % name)
768             self._set_config('active perspective', name)
769             self._c['manager'].LoadPerspective(self._perspectives[name])
770             self._c['manager'].Update()
771             for pane in self._c['manager'].GetAllPanes():
772                 view = self._c['menu bar']._c['view']
773                 if pane.name in view._c.keys():
774                     view._c[pane.name].Check(pane.window.IsShown())
775
776     def _on_save_perspective(self, *args):
777         perspective = self._c['manager'].SavePerspective()
778         name = self.gui.config['active perspective']
779         if name == 'Default':
780             name = 'New perspective'
781         name = select_save_file(
782             directory=os.path.expanduser(self.gui.config['perspective path']),
783             name=name,
784             extension=self.gui.config['perspective extension'],
785             parent=self,
786             message='Enter a name for the new perspective:',
787             caption='Save perspective')
788         if name == None:
789             return
790         self._save_perspective(
791             perspective,
792             os.path.expanduser(self.gui.config['perspective path']), name=name,
793             extension=self.gui.config['perspective extension'])
794
795     def _on_delete_perspective(self, *args, **kwargs):
796         options = sorted([p for p in self._perspectives.keys()
797                           if p != 'Default'])
798         dialog = SelectionDialog(
799             options=options,
800             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
801             button_id=wx.ID_DELETE,
802             selection_style='multiple',
803             parent=self,
804             title='Delete perspective(s)',
805             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
806         dialog.CenterOnScreen()
807         dialog.ShowModal()
808         if dialog.canceled == True:
809             return
810         names = [options[i] for i in dialog.selected]
811         dialog.Destroy()
812         self._delete_perspectives(
813             os.path.expanduser(self.gui.config['perspective path']),
814             names=names, extension=self.gui.config['perspective extension'])
815
816     def _on_select_perspective(self, _class, method, name):
817         self._restore_perspective(name)
818
819
820 # setup per-command versions of HookeFrame._update_curve
821 for _command in ['convert_distance_to_force',
822                  'polymer_fit_peaks',
823                  'remove_cantilever_from_extension',
824                  'zero_surface_contact_point',
825                  ]:
826     setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
827 del _command
828
829
830 class HookeApp (wx.App):
831     """A :class:`wx.App` wrapper around :class:`HookeFrame`.
832
833     Tosses up a splash screen and then loads :class:`HookeFrame` in
834     its own window.
835     """
836     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
837         self.gui = gui
838         self.commands = commands
839         self.inqueue = inqueue
840         self.outqueue = outqueue
841         super(HookeApp, self).__init__(*args, **kwargs)
842
843     def OnInit(self):
844         self.SetAppName('Hooke')
845         self.SetVendorName('')
846         self._setup_splash_screen()
847
848         height = self.gui.config['main height']
849         width = self.gui.config['main width']
850         top = self.gui.config['main top']
851         left = self.gui.config['main left']
852
853         # Sometimes, the ini file gets confused and sets 'left' and
854         # 'top' to large negative numbers.  Here we catch and fix
855         # this.  Keep small negative numbers, the user might want
856         # those.
857         if left < -width:
858             left = 0
859         if top < -height:
860             top = 0
861
862         self._c = {
863             'frame': HookeFrame(
864                 self.gui, self.commands, self.inqueue, self.outqueue,
865                 parent=None, title='Hooke',
866                 pos=(left, top), size=(width, height),
867                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
868             }
869         self._c['frame'].Show(True)
870         self.SetTopWindow(self._c['frame'])
871         return True
872
873     def _setup_splash_screen(self):
874         if self.gui.config['show splash screen'] == True:
875             path = os.path.expanduser(self.gui.config['splash screen image'])
876             if os.path.isfile(path):
877                 duration = self.gui.config['splash screen duration']
878                 wx.SplashScreen(
879                     bitmap=wx.Image(path).ConvertToBitmap(),
880                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
881                     milliseconds=duration,
882                     parent=None)
883                 wx.Yield()
884                 # For some reason splashDuration and sleep do not
885                 # correspond to each other at least not on Windows.
886                 # Maybe it's because duration is in milliseconds and
887                 # sleep in seconds.  Thus we need to increase the
888                 # sleep time a bit. A factor of 1.2 seems to work.
889                 sleepFactor = 1.2
890                 time.sleep(sleepFactor * duration / 1000)