1 # Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
3 # This file is part of Hooke.
5 # Hooke is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke. If not, see <http://www.gnu.org/licenses/>.
18 """Define :class:`HookeApp` and related, central application classes.
24 wxversion.select(WX_GOOD)
36 #import wx.aui as aui # C++ implementation
37 import wx.lib.agw.aui as aui # Python implementation
38 import wx.lib.evtmgr as evtmgr
39 # wxPropertyGrid is included in wxPython >= 2.9.1, see
40 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
41 # until then, we'll avoid it because of the *nix build problems.
42 #import wx.propgrid as wxpg
44 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
45 from ...engine import CommandMessage
46 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
47 from .dialog.selection import Selection as SelectionDialog
48 from .dialog.save_file import select_save_file
49 from . import menu as menu
50 from . import navbar as navbar
51 from . import panel as panel
52 from .panel.propertyeditor import props_from_argument, props_from_setting
53 from . import statusbar as statusbar
56 class HookeFrame (wx.Frame):
57 """The main Hooke-interface window.
59 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
60 super(HookeFrame, self).__init__(*args, **kwargs)
61 self.log = logging.getLogger('hooke')
63 self.commands = commands
64 self.inqueue = inqueue
65 self.outqueue = outqueue
66 self._perspectives = {} # {name: perspective_str}
70 os.path.expanduser(self.gui.config['icon image']),
74 self._c['manager'] = aui.AuiManager()
75 self._c['manager'].SetManagedWindow(self)
77 # set the gradient and drag styles
78 self._c['manager'].GetArtProvider().SetMetric(
79 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
81 # Min size for the frame itself isn't completely done. See
82 # the end of FrameManager::Update() for the test code. For
83 # now, just hard code a frame minimum size.
84 #self.SetMinSize(wx.Size(500, 500))
87 self._setup_toolbars()
88 self._c['manager'].Update() # commit pending changes
90 # Create the menubar after the panes so that the default
91 # perspective is created with all panes open
92 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
93 self._c['menu bar'] = menu.HookeMenuBar(
97 'close': self._on_close,
98 'about': self._on_about,
99 'view_panel': self._on_panel_visibility,
100 'save_perspective': self._on_save_perspective,
101 'delete_perspective': self._on_delete_perspective,
102 'select_perspective': self._on_select_perspective,
104 self.SetMenuBar(self._c['menu bar'])
106 self._c['status bar'] = statusbar.StatusBar(
108 style=wx.ST_SIZEGRIP)
109 self.SetStatusBar(self._c['status bar'])
111 self._setup_perspectives()
113 return # TODO: cleanup
114 self._displayed_plot = None
115 #load default list, if possible
116 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
121 def _setup_panels(self):
122 client_size = self.GetClientSize()
124 # ('folders', wx.GenericDirCtrl(
126 # dir=self.gui.config['folders-workdir'],
128 # style=wx.DIRCTRL_SHOW_FILTERS,
129 # filter=self.gui.config['folders-filters'],
130 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
131 (panel.PANELS['playlist'](
133 '_delete_playlist':self._delete_playlist,
134 '_delete_curve':self._delete_curve,
135 '_on_set_selected_playlist':self._on_set_selected_playlist,
136 '_on_set_selected_curve':self._on_set_selected_curve,
139 style=wx.WANTS_CHARS|wx.NO_BORDER,
140 # WANTS_CHARS so the panel doesn't eat the Return key.
143 (panel.PANELS['note'](
145 '_on_update':self._on_update_note,
148 style=wx.WANTS_CHARS|wx.NO_BORDER,
151 # ('notebook', Notebook(
153 # pos=wx.Point(client_size.x, client_size.y),
154 # size=wx.Size(430, 200),
155 # style=aui.AUI_NB_DEFAULT_STYLE
156 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
157 (panel.PANELS['commands'](
158 commands=self.commands,
159 selected=self.gui.config['selected command'],
161 'execute': self.explicit_execute_command,
162 'select_plugin': self.select_plugin,
163 'select_command': self.select_command,
164 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
167 style=wx.WANTS_CHARS|wx.NO_BORDER,
168 # WANTS_CHARS so the panel doesn't eat the Return key.
171 (panel.PANELS['propertyeditor'](
174 style=wx.WANTS_CHARS,
175 # WANTS_CHARS so the panel doesn't eat the Return key.
177 (panel.PANELS['plot'](
179 '_set_status_text': self._on_plot_status_text,
182 style=wx.WANTS_CHARS|wx.NO_BORDER,
183 # WANTS_CHARS so the panel doesn't eat the Return key.
186 (panel.PANELS['output'](
189 size=wx.Size(150, 90),
190 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
193 self._add_panel(p, style)
194 self.execute_command( # setup already loaded playlists
195 command=self._command_by_name('playlists'))
196 self.execute_command( # setup already loaded curve
197 command=self._command_by_name('get curve'))
199 def _add_panel(self, panel, style):
200 self._c[panel.name] = panel
201 m_name = panel.managed_name
202 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
203 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
206 elif style == 'center':
208 elif style == 'left':
210 elif style == 'right':
213 assert style == 'bottom', style
215 self._c['manager'].AddPane(panel, info)
217 def _setup_toolbars(self):
218 self._c['navigation bar'] = navbar.NavBar(
220 'next': self._next_curve,
221 'previous': self._previous_curve,
222 'delete': self._delete_curve,
225 self._c['manager'].AddPane(
226 self._c['navigation bar'],
227 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
228 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
229 ).RightDockable(False))
231 def _bind_events(self):
232 # TODO: figure out if we can use the eventManager for menu
233 # ranges and events of 'self' without raising an assertion
235 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
236 self.Bind(wx.EVT_SIZE, self._on_size)
237 self.Bind(wx.EVT_CLOSE, self._on_close)
238 self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
239 self.Bind(wx.EVT_CHAR_HOOK, self._on_key)
241 return # TODO: cleanup
242 treeCtrl = self._c['folders'].GetTreeCtrl()
243 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
245 def _on_about(self, *args):
246 dialog = wx.MessageDialog(
248 message=self.gui._splash_text(extra_info={
249 'get-details':'click "Help -> License"'},
251 caption='About Hooke',
252 style=wx.OK|wx.ICON_INFORMATION)
256 def _on_size(self, event):
259 def _on_close(self, *args):
260 self.log.info('closing GUI framework')
263 self._set_config('main height', self.GetSize().GetHeight())
264 self._set_config('main left', self.GetPosition()[0])
265 self._set_config('main top', self.GetPosition()[1])
266 self._set_config('main width', self.GetSize().GetWidth())
267 self._c['manager'].UnInit()
268 del self._c['manager']
271 def _on_erase_background(self, event):
274 def _on_key(self, event):
275 code = event.GetKeyCode()
276 if code == wx.WXK_RIGHT:
278 elif code == wx.WXK_LEFT:
279 self._previous_curve()
280 elif code == wx.WXK_DELETE or code == wx.WXK_BACK:
286 # Panel utility functions
288 def _file_name(self, name):
289 """Cleanup names according to configured preferences.
291 if self.gui.config['hide extensions'] == True:
292 name,ext = os.path.splitext(name)
299 def _command_by_name(self, name):
300 cs = [c for c in self.commands if c.name == name]
304 raise Exception('Multiple commands named "%s"' % name)
307 def explicit_execute_command(self, _class=None, method=None,
308 command=None, args=None):
309 return self.execute_command(
310 _class=_class, method=method, command=command, args=args,
311 explicit_user_call=True)
313 def execute_command(self, _class=None, method=None,
314 command=None, args=None, explicit_user_call=False):
317 if ('property editor' in self._c
318 and self.gui.config['selected command'] == command.name):
319 for name,value in self._c['property editor'].get_values().items():
320 arg = self._c['property editor']._argument_from_label.get(
325 args[arg.name] = value
327 # deal with counted arguments
328 if arg.name not in args:
330 index = int(name[len(arg.name):])
331 args[arg.name][index] = value
332 for arg in command.arguments:
333 if arg.name not in args:
334 continue # undisplayed argument, e.g. 'driver' types.
336 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
337 count = arg._display_count
338 if count != 1 and arg.name in args:
339 keys = sorted(args[arg.name].keys())
340 assert keys == range(count), keys
341 args[arg.name] = [args[arg.name][i]
342 for i in range(count)]
344 while (len(args[arg.name]) > 0
345 and args[arg.name][-1] == None):
347 if len(args[arg.name]) == 0:
348 args[arg.name] = arg.default
349 cm = CommandMessage(command.name, args)
350 self.gui._submit_command(
351 cm, self.inqueue, explicit_user_call=explicit_user_call)
352 # TODO: skip responses for commands that were captured by the
353 # command stack. We'd need to poll on each request, remember
354 # capture state, or add a flag to the response...
355 return self._handle_response(command_message=cm)
357 def _handle_response(self, command_message):
360 msg = self.outqueue.get()
362 if isinstance(msg, Exit):
365 elif isinstance(msg, CommandExit):
366 # TODO: display command complete
368 elif isinstance(msg, ReloadUserInterfaceConfig):
369 self.gui.reload_config(msg.config)
371 elif isinstance(msg, Request):
372 h = handler.HANDLERS[msg.type]
373 h.run(self, msg) # TODO: pause for response?
376 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
377 self._postprocess_text)
378 pp(command=command_message.command,
379 args=command_message.arguments,
383 def _handle_request(self, msg):
384 """Repeatedly try to get a response to `msg`.
387 raise NotImplementedError('_%s_request_prompt' % msg.type)
388 prompt_string = prompt(msg)
389 parser = getattr(self, '_%s_request_parser' % msg.type, None)
391 raise NotImplementedError('_%s_request_parser' % msg.type)
395 self.cmd.stdout.write(''.join([
396 error.__class__.__name__, ': ', str(error), '\n']))
397 self.cmd.stdout.write(prompt_string)
398 value = parser(msg, self.cmd.stdin.readline())
400 response = msg.response(value)
402 except ValueError, error:
404 self.inqueue.put(response)
406 def _set_config(self, option, value, section=None):
407 self.gui._set_config(section=section, option=option, value=value,
408 ui_to_command_queue=self.inqueue,
409 response_handler=self._handle_response)
412 # Command-specific postprocessing
414 def _postprocess_text(self, command, args={}, results=[]):
415 """Print the string representation of the results to the Results window.
417 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
418 approach, except that :class:`~hooke.ui.commandline.DoCommand`
419 doesn't print some internally handled messages
420 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
422 for result in results:
423 if isinstance(result, CommandExit):
424 self._c['output'].write(result.__class__.__name__+'\n')
425 self._c['output'].write(str(result).rstrip()+'\n')
427 def _postprocess_playlists(self, command, args={}, results=None):
428 """Update `self` to show the playlists.
430 if not isinstance(results[-1], Success):
431 self._postprocess_text(command, results=results)
433 assert len(results) == 2, results
434 playlists = results[0]
435 if 'playlist' in self._c:
436 for playlist in playlists:
437 if self._c['playlist'].is_playlist_loaded(playlist):
438 self._c['playlist'].update_playlist(playlist)
440 self._c['playlist'].add_playlist(playlist)
442 def _postprocess_new_playlist(self, command, args={}, results=None):
443 """Update `self` to show the new playlist.
445 if not isinstance(results[-1], Success):
446 self._postprocess_text(command, results=results)
448 assert len(results) == 2, results
449 playlist = results[0]
450 if 'playlist' in self._c:
451 loaded = self._c['playlist'].is_playlist_loaded(playlist)
452 assert loaded == False, loaded
453 self._c['playlist'].add_playlist(playlist)
455 def _postprocess_load_playlist(self, command, args={}, results=None):
456 """Update `self` to show the playlist.
458 if not isinstance(results[-1], Success):
459 self._postprocess_text(command, results=results)
461 assert len(results) == 2, results
462 playlist = results[0]
463 self._c['playlist'].add_playlist(playlist)
465 def _postprocess_get_playlist(self, command, args={}, results=[]):
466 if not isinstance(results[-1], Success):
467 self._postprocess_text(command, results=results)
469 assert len(results) == 2, results
470 playlist = results[0]
471 if 'playlist' in self._c:
472 loaded = self._c['playlist'].is_playlist_loaded(playlist)
473 assert loaded == True, loaded
474 self._c['playlist'].update_playlist(playlist)
476 def _postprocess_name_playlist(self, command, args={}, results=None):
477 """Update `self` to show the new playlist.
479 return self._postprocess_new_playlist(command, args, results)
481 def _postprocess_get_curve(self, command, args={}, results=[]):
482 """Update `self` to show the curve.
484 if not isinstance(results[-1], Success):
485 self._postprocess_text(command, results=results)
487 assert len(results) == 2, results
489 if args.get('curve', None) == None:
490 # the command defaults to the current curve of the current playlist
491 results = self.execute_command(
492 command=self._command_by_name('get playlist'))
493 playlist = results[0]
495 raise NotImplementedError()
496 if 'note' in self._c:
497 self._c['note'].set_text(curve.info.get('note', ''))
498 if 'playlist' in self._c:
499 self._c['playlist'].set_selected_curve(
501 if 'plot' in self._c:
502 self._c['plot'].set_curve(curve, config=self.gui.config)
504 def _postprocess_next_curve(self, command, args={}, results=[]):
505 """No-op. Only call 'next curve' via `self._next_curve()`.
509 def _postprocess_previous_curve(self, command, args={}, results=[]):
510 """No-op. Only call 'previous curve' via `self._previous_curve()`.
514 def _postprocess_glob_curves_to_playlist(
515 self, command, args={}, results=[]):
516 """Update `self` to show new curves.
518 if not isinstance(results[-1], Success):
519 self._postprocess_text(command, results=results)
521 if 'playlist' in self._c:
522 if args.get('playlist', None) != None:
523 playlist = args['playlist']
524 pname = playlist.name
525 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
526 assert loaded == True, loaded
527 for curve in results[:-1]:
528 self._c['playlist']._add_curve(pname, curve)
530 self.execute_command(
531 command=self._command_by_name('get playlist'))
533 def _postprocess_delete_curve(self, command, args={}, results=[]):
534 """No-op. Only call 'delete curve' via `self._delete_curve()`.
538 def _update_curve(self, command, args={}, results=[]):
539 """Update the curve, since the available columns may have changed.
541 if isinstance(results[-1], Success):
542 self.execute_command(
543 command=self._command_by_name('get curve'))
546 # Command panel interface
548 def select_command(self, _class, method, command):
549 #self.select_plugin(plugin=command.plugin)
550 self._c['property editor'].clear()
551 self._c['property editor']._argument_from_label = {}
552 for argument in command.arguments:
553 if argument.name == 'help':
556 results = self.execute_command(
557 command=self._command_by_name('playlists'))
558 if not isinstance(results[-1], Success):
559 self._postprocess_text(command, results=results)
562 playlists = results[0]
564 results = self.execute_command(
565 command=self._command_by_name('playlist curves'))
566 if not isinstance(results[-1], Success):
567 self._postprocess_text(command, results=results)
572 ret = props_from_argument(
573 argument, curves=curves, playlists=playlists)
575 continue # property intentionally not handled (yet)
577 self._c['property editor'].append_property(p)
578 self._c['property editor']._argument_from_label[label] = (
581 self._set_config('selected command', command.name)
583 def select_plugin(self, _class=None, method=None, plugin=None):
588 # Folders panel interface
590 def _on_dir_ctrl_left_double_click(self, event):
591 file_path = self.panelFolders.GetPath()
592 if os.path.isfile(file_path):
593 if file_path.endswith('.hkp'):
594 self.do_loadlist(file_path)
599 # Note panel interface
601 def _on_update_note(self, _class, method, text):
602 """Sets the note for the active curve.
604 self.execute_command(
605 command=self._command_by_name('set note'),
610 # Playlist panel interface
612 def _delete_playlist(self, _class, method, playlist):
613 #if hasattr(playlist, 'path') and playlist.path != None:
614 # os.remove(playlist.path)
615 # TODO: remove playlist from underlying hooke instance and call ._c['playlist'].delete_playlist()
616 # possibly rename this method to _postprocess_delete_playlist...
619 def _on_set_selected_playlist(self, _class, method, playlist):
620 """Call the `jump to playlist` command.
622 results = self.execute_command(
623 command=self._command_by_name('playlists'))
624 if not isinstance(results[-1], Success):
626 assert len(results) == 2, results
627 playlists = results[0]
628 matching = [p for p in playlists if p.name == playlist.name]
629 assert len(matching) == 1, matching
630 index = playlists.index(matching[0])
631 results = self.execute_command(
632 command=self._command_by_name('jump to playlist'),
633 args={'index':index})
635 def _on_set_selected_curve(self, _class, method, playlist, curve):
636 """Call the `jump to curve` command.
638 self._on_set_selected_playlist(_class, method, playlist)
639 index = playlist.index(curve)
640 results = self.execute_command(
641 command=self._command_by_name('jump to curve'),
642 args={'index':index})
643 if not isinstance(results[-1], Success):
645 #results = self.execute_command(
646 # command=self._command_by_name('get playlist'))
647 #if not isinstance(results[-1], Success):
649 self.execute_command(
650 command=self._command_by_name('get curve'))
654 # Plot panel interface
656 def _on_plot_status_text(self, _class, method, text):
657 if 'status bar' in self._c:
658 self._c['status bar'].set_plot_text(text)
664 def _next_curve(self, *args):
665 """Call the `next curve` command.
667 results = self.execute_command(
668 command=self._command_by_name('next curve'))
669 if isinstance(results[-1], Success):
670 self.execute_command(
671 command=self._command_by_name('get curve'))
673 def _previous_curve(self, *args):
674 """Call the `previous curve` command.
676 results = self.execute_command(
677 command=self._command_by_name('previous curve'))
678 if isinstance(results[-1], Success):
679 self.execute_command(
680 command=self._command_by_name('get curve'))
682 def _delete_curve(self, *args, **kwargs):
684 playlist = kwargs.get('playlist', None)
685 curve = kwargs.get('curve', None)
686 if playlist is not None and curve is not None:
687 cmd_kwargs['index'] = playlist.index(curve)
688 results = self.execute_command(
689 command=self._command_by_name('remove curve from playlist'),
691 if isinstance(results[-1], Success):
692 results = self.execute_command(
693 command=self._command_by_name('get playlist'))
696 # Panel display handling
698 def _on_pane_close(self, event):
700 view = self._c['menu bar']._c['view']
701 if pane.name in view._c.keys():
702 view._c[pane.name].Check(False)
705 def _on_panel_visibility(self, _class, method, panel_name, visible):
706 pane = self._c['manager'].GetPane(panel_name)
708 #if we don't do the following, the Folders pane does not resize properly on hide/show
709 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
710 #folders_size = pane.GetSize()
711 self.panelFolders.Fit()
712 self._c['manager'].Update()
714 def _setup_perspectives(self):
715 """Add perspectives to menubar and _perspectives.
717 self._perspectives = {
718 'Default': self._c['manager'].SavePerspective(),
720 path = os.path.expanduser(self.gui.config['perspective path'])
721 if os.path.isdir(path):
722 files = sorted(os.listdir(path))
724 name, extension = os.path.splitext(fname)
725 if extension != self.gui.config['perspective extension']:
727 fpath = os.path.join(path, fname)
728 if not os.path.isfile(fpath):
731 with open(fpath, 'rU') as f:
732 perspective = f.readline()
734 self._perspectives[name] = perspective
736 selected_perspective = self.gui.config['active perspective']
737 if not self._perspectives.has_key(selected_perspective):
738 self._set_config('active perspective', 'Default')
740 self._restore_perspective(selected_perspective, force=True)
741 self._update_perspective_menu()
743 def _update_perspective_menu(self):
744 self._c['menu bar']._c['perspective'].update(
745 sorted(self._perspectives.keys()),
746 self.gui.config['active perspective'])
748 def _save_perspective(self, perspective, perspective_dir, name,
750 path = os.path.join(perspective_dir, name)
751 if extension != None:
753 if not os.path.isdir(perspective_dir):
754 os.makedirs(perspective_dir)
755 with open(path, 'w') as f:
757 self._perspectives[name] = perspective
758 self._restore_perspective(name)
759 self._update_perspective_menu()
761 def _delete_perspectives(self, perspective_dir, names,
763 self.log.debug('remove perspectives %s from %s'
764 % (names, perspective_dir))
766 path = os.path.join(perspective_dir, name)
767 if extension != None:
770 del(self._perspectives[name])
771 self._update_perspective_menu()
772 if self.gui.config['active perspective'] in names:
773 self._restore_perspective('Default')
774 # TODO: does this bug still apply?
775 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
776 # http://trac.wxwidgets.org/ticket/3258
777 # ) that makes the radio item indicator in the menu disappear.
778 # The code should be fine once this issue is fixed.
780 def _restore_perspective(self, name, force=False):
781 if name != self.gui.config['active perspective'] or force == True:
782 self.log.debug('restore perspective %s' % name)
783 self._set_config('active perspective', name)
784 self._c['manager'].LoadPerspective(self._perspectives[name])
785 self._c['manager'].Update()
786 for pane in self._c['manager'].GetAllPanes():
787 view = self._c['menu bar']._c['view']
788 if pane.name in view._c.keys():
789 view._c[pane.name].Check(pane.window.IsShown())
791 def _on_save_perspective(self, *args):
792 perspective = self._c['manager'].SavePerspective()
793 name = self.gui.config['active perspective']
794 if name == 'Default':
795 name = 'New perspective'
796 name = select_save_file(
797 directory=os.path.expanduser(self.gui.config['perspective path']),
799 extension=self.gui.config['perspective extension'],
801 message='Enter a name for the new perspective:',
802 caption='Save perspective')
805 self._save_perspective(
807 os.path.expanduser(self.gui.config['perspective path']), name=name,
808 extension=self.gui.config['perspective extension'])
810 def _on_delete_perspective(self, *args, **kwargs):
811 options = sorted([p for p in self._perspectives.keys()
813 dialog = SelectionDialog(
815 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
816 button_id=wx.ID_DELETE,
817 selection_style='multiple',
819 title='Delete perspective(s)',
820 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
821 dialog.CenterOnScreen()
823 if dialog.canceled == True:
825 names = [options[i] for i in dialog.selected]
827 self._delete_perspectives(
828 os.path.expanduser(self.gui.config['perspective path']),
829 names=names, extension=self.gui.config['perspective extension'])
831 def _on_select_perspective(self, _class, method, name):
832 self._restore_perspective(name)
835 # setup per-command versions of HookeFrame._update_curve
836 for _command in ['convert_distance_to_force',
838 'remove_cantilever_from_extension',
839 'zero_surface_contact_point',
841 setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
845 class HookeApp (wx.App):
846 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
848 Tosses up a splash screen and then loads :class:`HookeFrame` in
851 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
853 self.commands = commands
854 self.inqueue = inqueue
855 self.outqueue = outqueue
856 super(HookeApp, self).__init__(*args, **kwargs)
859 self.SetAppName('Hooke')
860 self.SetVendorName('')
861 self._setup_splash_screen()
863 height = self.gui.config['main height']
864 width = self.gui.config['main width']
865 top = self.gui.config['main top']
866 left = self.gui.config['main left']
868 # Sometimes, the ini file gets confused and sets 'left' and
869 # 'top' to large negative numbers. Here we catch and fix
870 # this. Keep small negative numbers, the user might want
879 self.gui, self.commands, self.inqueue, self.outqueue,
880 parent=None, title='Hooke',
881 pos=(left, top), size=(width, height),
882 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
884 self._c['frame'].Show(True)
885 self.SetTopWindow(self._c['frame'])
888 def _setup_splash_screen(self):
889 if self.gui.config['show splash screen'] == True:
890 path = os.path.expanduser(self.gui.config['splash screen image'])
891 if os.path.isfile(path):
892 duration = self.gui.config['splash screen duration']
894 bitmap=wx.Image(path).ConvertToBitmap(),
895 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
896 milliseconds=duration,
899 # For some reason splashDuration and sleep do not
900 # correspond to each other at least not on Windows.
901 # Maybe it's because duration is in milliseconds and
902 # sleep in seconds. Thus we need to increase the
903 # sleep time a bit. A factor of 1.2 seems to work.
905 time.sleep(sleepFactor * duration / 1000)