1 # Copyright (C) 2010-2012 W. Trevor King <wking@tremily.us>
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.
31 #import wx.aui as aui # C++ implementation
32 import wx.lib.agw.aui as aui # Python implementation
33 # wxPropertyGrid is included in wxPython >= 2.9.1, see
34 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
35 # until then, we'll avoid it because of the *nix build problems.
36 #import wx.propgrid as wxpg
38 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
39 from ...engine import CommandMessage
40 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
41 from .dialog.selection import Selection as SelectionDialog
42 from .dialog.save_file import select_save_file
43 from . import menu as menu
44 from . import navbar as navbar
45 from . import panel as panel
46 from .panel.propertyeditor import props_from_argument, props_from_setting
47 from . import statusbar as statusbar
50 class HookeFrame (wx.Frame):
51 """The main Hooke-interface window.
53 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
54 super(HookeFrame, self).__init__(*args, **kwargs)
55 self.log = logging.getLogger('hooke')
57 self.commands = commands
58 self.inqueue = inqueue
59 self.outqueue = outqueue
60 self._perspectives = {} # {name: perspective_str}
64 os.path.expanduser(self.gui.config['icon image']),
68 self._c['manager'] = aui.AuiManager()
69 self._c['manager'].SetManagedWindow(self)
71 # set the gradient and drag styles
72 self._c['manager'].GetArtProvider().SetMetric(
73 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
75 # Min size for the frame itself isn't completely done. See
76 # the end of FrameManager::Update() for the test code. For
77 # now, just hard code a frame minimum size.
78 #self.SetMinSize(wx.Size(500, 500))
81 self._setup_toolbars()
82 self._c['manager'].Update() # commit pending changes
84 # Create the menubar after the panes so that the default
85 # perspective is created with all panes open
86 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
87 self._c['menu bar'] = menu.HookeMenuBar(
91 'close': self._on_close,
92 'about': self._on_about,
93 'view_panel': self._on_panel_visibility,
94 'save_perspective': self._on_save_perspective,
95 'delete_perspective': self._on_delete_perspective,
96 'select_perspective': self._on_select_perspective,
98 self.SetMenuBar(self._c['menu bar'])
100 self._c['status bar'] = statusbar.StatusBar(
102 style=wx.ST_SIZEGRIP)
103 self.SetStatusBar(self._c['status bar'])
105 self._setup_perspectives()
107 return # TODO: cleanup
108 self._displayed_plot = None
109 #load default list, if possible
110 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
115 def _setup_panels(self):
116 client_size = self.GetClientSize()
118 # ('folders', wx.GenericDirCtrl(
120 # dir=self.gui.config['folders-workdir'],
122 # style=wx.DIRCTRL_SHOW_FILTERS,
123 # filter=self.gui.config['folders-filters'],
124 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
125 (panel.PANELS['playlist'](
127 '_delete_playlist':self._delete_playlist,
128 '_delete_curve':self._delete_curve,
129 '_on_set_selected_playlist':self._on_set_selected_playlist,
130 '_on_set_selected_curve':self._on_set_selected_curve,
133 style=wx.WANTS_CHARS|wx.NO_BORDER,
134 # WANTS_CHARS so the panel doesn't eat the Return key.
137 (panel.PANELS['note'](
139 '_on_update':self._on_update_note,
142 style=wx.WANTS_CHARS|wx.NO_BORDER,
145 # ('notebook', Notebook(
147 # pos=wx.Point(client_size.x, client_size.y),
148 # size=wx.Size(430, 200),
149 # style=aui.AUI_NB_DEFAULT_STYLE
150 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
151 (panel.PANELS['commands'](
152 commands=self.commands,
153 selected=self.gui.config['selected command'],
155 'execute': self.explicit_execute_command,
156 'select_plugin': self.select_plugin,
157 'select_command': self.select_command,
158 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
161 style=wx.WANTS_CHARS|wx.NO_BORDER,
162 # WANTS_CHARS so the panel doesn't eat the Return key.
165 (panel.PANELS['propertyeditor'](
168 style=wx.WANTS_CHARS,
169 # WANTS_CHARS so the panel doesn't eat the Return key.
171 (panel.PANELS['plot'](
173 '_set_status_text': self._on_plot_status_text,
176 style=wx.WANTS_CHARS|wx.NO_BORDER,
177 # WANTS_CHARS so the panel doesn't eat the Return key.
180 (panel.PANELS['output'](
183 size=wx.Size(150, 90),
184 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
187 self._add_panel(p, style)
188 self.execute_command( # setup already loaded playlists
189 command=self._command_by_name('playlists'))
190 self.execute_command( # setup already loaded curve
191 command=self._command_by_name('get curve'))
193 def _add_panel(self, panel, style):
194 self._c[panel.name] = panel
195 m_name = panel.managed_name
196 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
197 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
200 elif style == 'center':
202 elif style == 'left':
204 elif style == 'right':
207 assert style == 'bottom', style
209 self._c['manager'].AddPane(panel, info)
211 def _setup_toolbars(self):
212 self._c['navigation bar'] = navbar.NavBar(
214 'next': self._next_curve,
215 'previous': self._previous_curve,
216 'delete': self._delete_curve,
219 self._c['manager'].AddPane(
220 self._c['navigation bar'],
221 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
222 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
223 ).RightDockable(False))
225 def _bind_events(self):
226 # TODO: figure out if we can use the eventManager for menu
227 # ranges and events of 'self' without raising an assertion
229 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
230 self.Bind(wx.EVT_SIZE, self._on_size)
231 self.Bind(wx.EVT_CLOSE, self._on_close)
232 self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
233 self.Bind(wx.EVT_CHAR_HOOK, self._on_key)
235 return # TODO: cleanup
236 treeCtrl = self._c['folders'].GetTreeCtrl()
237 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
239 def _on_about(self, *args):
240 dialog = wx.MessageDialog(
242 message=self.gui._splash_text(extra_info={
243 'get-details':'click "Help -> License"'},
245 caption='About Hooke',
246 style=wx.OK|wx.ICON_INFORMATION)
250 def _on_size(self, event):
253 def _on_close(self, *args):
254 self.log.info('closing GUI framework')
257 self._set_config('main height', self.GetSize().GetHeight())
258 self._set_config('main left', self.GetPosition()[0])
259 self._set_config('main top', self.GetPosition()[1])
260 self._set_config('main width', self.GetSize().GetWidth())
261 self._c['manager'].UnInit()
262 del self._c['manager']
265 def _on_erase_background(self, event):
268 def _on_key(self, event):
269 code = event.GetKeyCode()
270 if code == wx.WXK_RIGHT:
272 elif code == wx.WXK_LEFT:
273 self._previous_curve()
274 elif code == wx.WXK_DELETE or code == wx.WXK_BACK:
280 # Panel utility functions
282 def _file_name(self, name):
283 """Cleanup names according to configured preferences.
285 if self.gui.config['hide extensions'] == True:
286 name,ext = os.path.splitext(name)
293 def _command_by_name(self, name):
294 cs = [c for c in self.commands if c.name == name]
298 raise Exception('Multiple commands named "%s"' % name)
301 def explicit_execute_command(self, _class=None, method=None,
302 command=None, args=None):
303 return self.execute_command(
304 _class=_class, method=method, command=command, args=args,
305 explicit_user_call=True)
307 def execute_command(self, _class=None, method=None,
308 command=None, args=None, explicit_user_call=False):
311 if ('property editor' in self._c
312 and self.gui.config['selected command'] == command.name):
313 for name,value in self._c['property editor'].get_values().items():
314 arg = self._c['property editor']._argument_from_label.get(
319 args[arg.name] = value
321 # deal with counted arguments
322 if arg.name not in args:
324 index = int(name[len(arg.name):])
325 args[arg.name][index] = value
326 for arg in command.arguments:
327 if arg.name not in args:
328 continue # undisplayed argument, e.g. 'driver' types.
330 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
331 count = arg._display_count
332 if count != 1 and arg.name in args:
333 keys = sorted(args[arg.name].keys())
334 assert keys == range(count), keys
335 args[arg.name] = [args[arg.name][i]
336 for i in range(count)]
338 while (len(args[arg.name]) > 0
339 and args[arg.name][-1] == None):
341 if len(args[arg.name]) == 0:
342 args[arg.name] = arg.default
343 cm = CommandMessage(command.name, args)
344 self.gui._submit_command(
345 cm, self.inqueue, explicit_user_call=explicit_user_call)
346 # TODO: skip responses for commands that were captured by the
347 # command stack. We'd need to poll on each request, remember
348 # capture state, or add a flag to the response...
349 return self._handle_response(command_message=cm)
351 def _handle_response(self, command_message):
354 msg = self.outqueue.get()
356 if isinstance(msg, Exit):
359 elif isinstance(msg, CommandExit):
360 # TODO: display command complete
362 elif isinstance(msg, ReloadUserInterfaceConfig):
363 self.gui.reload_config(msg.config)
365 elif isinstance(msg, Request):
366 h = handler.HANDLERS[msg.type]
367 h.run(self, msg) # TODO: pause for response?
370 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
371 self._postprocess_text)
372 pp(command=command_message.command,
373 args=command_message.arguments,
377 def _handle_request(self, msg):
378 """Repeatedly try to get a response to `msg`.
381 raise NotImplementedError('_%s_request_prompt' % msg.type)
382 prompt_string = prompt(msg)
383 parser = getattr(self, '_%s_request_parser' % msg.type, None)
385 raise NotImplementedError('_%s_request_parser' % msg.type)
389 self.cmd.stdout.write(''.join([
390 error.__class__.__name__, ': ', str(error), '\n']))
391 self.cmd.stdout.write(prompt_string)
392 value = parser(msg, self.cmd.stdin.readline())
394 response = msg.response(value)
396 except ValueError, error:
398 self.inqueue.put(response)
400 def _set_config(self, option, value, section=None):
401 self.gui._set_config(section=section, option=option, value=value,
402 ui_to_command_queue=self.inqueue,
403 response_handler=self._handle_response)
406 # Command-specific postprocessing
408 def _postprocess_text(self, command, args={}, results=[]):
409 """Print the string representation of the results to the Results window.
411 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
412 approach, except that :class:`~hooke.ui.commandline.DoCommand`
413 doesn't print some internally handled messages
414 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
416 for result in results:
417 if isinstance(result, CommandExit):
418 self._c['output'].write(result.__class__.__name__+'\n')
419 self._c['output'].write(str(result).rstrip()+'\n')
421 def _postprocess_playlists(self, command, args={}, results=None):
422 """Update `self` to show the playlists.
424 if not isinstance(results[-1], Success):
425 self._postprocess_text(command, results=results)
427 assert len(results) == 2, results
428 playlists = results[0]
429 if 'playlist' in self._c:
430 for playlist in playlists:
431 if self._c['playlist'].is_playlist_loaded(playlist):
432 self._c['playlist'].update_playlist(playlist)
434 self._c['playlist'].add_playlist(playlist)
436 def _postprocess_new_playlist(self, command, args={}, results=None):
437 """Update `self` to show the new playlist.
439 if not isinstance(results[-1], Success):
440 self._postprocess_text(command, results=results)
442 assert len(results) == 2, results
443 playlist = results[0]
444 if 'playlist' in self._c:
445 loaded = self._c['playlist'].is_playlist_loaded(playlist)
446 assert loaded == False, loaded
447 self._c['playlist'].add_playlist(playlist)
449 def _postprocess_load_playlist(self, command, args={}, results=None):
450 """Update `self` to show the playlist.
452 if not isinstance(results[-1], Success):
453 self._postprocess_text(command, results=results)
455 assert len(results) == 2, results
456 playlist = results[0]
457 self._c['playlist'].add_playlist(playlist)
459 def _postprocess_get_playlist(self, command, args={}, results=[]):
460 if not isinstance(results[-1], Success):
461 self._postprocess_text(command, results=results)
463 assert len(results) == 2, results
464 playlist = results[0]
465 if 'playlist' in self._c:
466 loaded = self._c['playlist'].is_playlist_loaded(playlist)
467 assert loaded == True, loaded
468 self._c['playlist'].update_playlist(playlist)
470 def _postprocess_name_playlist(self, command, args={}, results=None):
471 """Update `self` to show the new playlist.
473 return self._postprocess_new_playlist(command, args, results)
475 def _postprocess_get_curve(self, command, args={}, results=[]):
476 """Update `self` to show the curve.
478 if not isinstance(results[-1], Success):
479 self._postprocess_text(command, results=results)
481 assert len(results) == 2, results
483 if args.get('curve', None) == None:
484 # the command defaults to the current curve of the current playlist
485 results = self.execute_command(
486 command=self._command_by_name('get playlist'))
487 playlist = results[0]
489 raise NotImplementedError()
490 if 'note' in self._c:
491 self._c['note'].set_text(curve.info.get('note', ''))
492 if 'playlist' in self._c:
493 self._c['playlist'].set_selected_curve(
495 if 'plot' in self._c:
496 self._c['plot'].set_curve(curve, config=self.gui.config)
498 def _postprocess_next_curve(self, command, args={}, results=[]):
499 """No-op. Only call 'next curve' via `self._next_curve()`.
503 def _postprocess_previous_curve(self, command, args={}, results=[]):
504 """No-op. Only call 'previous curve' via `self._previous_curve()`.
508 def _postprocess_glob_curves_to_playlist(
509 self, command, args={}, results=[]):
510 """Update `self` to show new curves.
512 if not isinstance(results[-1], Success):
513 self._postprocess_text(command, results=results)
515 if 'playlist' in self._c:
516 if args.get('playlist', None) != None:
517 playlist = args['playlist']
518 pname = playlist.name
519 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
520 assert loaded == True, loaded
521 for curve in results[:-1]:
522 self._c['playlist']._add_curve(pname, curve)
524 self.execute_command(
525 command=self._command_by_name('get playlist'))
527 def _postprocess_delete_curve(self, command, args={}, results=[]):
528 """No-op. Only call 'delete curve' via `self._delete_curve()`.
532 def _update_curve(self, command, args={}, results=[]):
533 """Update the curve, since the available columns may have changed.
535 if isinstance(results[-1], Success):
536 self.execute_command(
537 command=self._command_by_name('get curve'))
540 # Command panel interface
542 def select_command(self, _class, method, command):
543 #self.select_plugin(plugin=command.plugin)
544 self._c['property editor'].clear()
545 self._c['property editor']._argument_from_label = {}
546 for argument in command.arguments:
547 if argument.name == 'help':
550 results = self.execute_command(
551 command=self._command_by_name('playlists'))
552 if not isinstance(results[-1], Success):
553 self._postprocess_text(command, results=results)
556 playlists = results[0]
558 results = self.execute_command(
559 command=self._command_by_name('playlist curves'))
560 if not isinstance(results[-1], Success):
561 self._postprocess_text(command, results=results)
566 ret = props_from_argument(
567 argument, curves=curves, playlists=playlists)
569 continue # property intentionally not handled (yet)
571 self._c['property editor'].append_property(p)
572 self._c['property editor']._argument_from_label[label] = (
575 self._set_config('selected command', command.name)
577 def select_plugin(self, _class=None, method=None, plugin=None):
582 # Folders panel interface
584 def _on_dir_ctrl_left_double_click(self, event):
585 file_path = self.panelFolders.GetPath()
586 if os.path.isfile(file_path):
587 if file_path.endswith('.hkp'):
588 self.do_loadlist(file_path)
593 # Note panel interface
595 def _on_update_note(self, _class, method, text):
596 """Sets the note for the active curve.
598 self.execute_command(
599 command=self._command_by_name('set note'),
604 # Playlist panel interface
606 def _delete_playlist(self, _class, method, playlist):
607 #if hasattr(playlist, 'path') and playlist.path != None:
608 # os.remove(playlist.path)
609 # TODO: remove playlist from underlying hooke instance and call ._c['playlist'].delete_playlist()
610 # possibly rename this method to _postprocess_delete_playlist...
613 def _on_set_selected_playlist(self, _class, method, playlist):
614 """Call the `jump to playlist` command.
616 results = self.execute_command(
617 command=self._command_by_name('playlists'))
618 if not isinstance(results[-1], Success):
620 assert len(results) == 2, results
621 playlists = results[0]
622 matching = [p for p in playlists if p.name == playlist.name]
623 assert len(matching) == 1, matching
624 index = playlists.index(matching[0])
625 results = self.execute_command(
626 command=self._command_by_name('jump to playlist'),
627 args={'index':index})
629 def _on_set_selected_curve(self, _class, method, playlist, curve):
630 """Call the `jump to curve` command.
632 self._on_set_selected_playlist(_class, method, playlist)
633 index = playlist.index(curve)
634 results = self.execute_command(
635 command=self._command_by_name('jump to curve'),
636 args={'index':index})
637 if not isinstance(results[-1], Success):
639 #results = self.execute_command(
640 # command=self._command_by_name('get playlist'))
641 #if not isinstance(results[-1], Success):
643 self.execute_command(
644 command=self._command_by_name('get curve'))
648 # Plot panel interface
650 def _on_plot_status_text(self, _class, method, text):
651 if 'status bar' in self._c:
652 self._c['status bar'].set_plot_text(text)
658 def _next_curve(self, *args):
659 """Call the `next curve` command.
661 results = self.execute_command(
662 command=self._command_by_name('next curve'))
663 if isinstance(results[-1], Success):
664 self.execute_command(
665 command=self._command_by_name('get curve'))
667 def _previous_curve(self, *args):
668 """Call the `previous curve` command.
670 results = self.execute_command(
671 command=self._command_by_name('previous curve'))
672 if isinstance(results[-1], Success):
673 self.execute_command(
674 command=self._command_by_name('get curve'))
676 def _delete_curve(self, *args, **kwargs):
678 playlist = kwargs.get('playlist', None)
679 curve = kwargs.get('curve', None)
680 if playlist is not None and curve is not None:
681 cmd_kwargs['index'] = playlist.index(curve)
682 results = self.execute_command(
683 command=self._command_by_name('remove curve from playlist'),
685 if isinstance(results[-1], Success):
686 results = self.execute_command(
687 command=self._command_by_name('get playlist'))
690 # Panel display handling
692 def _on_pane_close(self, event):
694 view = self._c['menu bar']._c['view']
695 if pane.name in view._c.keys():
696 view._c[pane.name].Check(False)
699 def _on_panel_visibility(self, _class, method, panel_name, visible):
700 pane = self._c['manager'].GetPane(panel_name)
702 #if we don't do the following, the Folders pane does not resize properly on hide/show
703 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
704 #folders_size = pane.GetSize()
705 self.panelFolders.Fit()
706 self._c['manager'].Update()
708 def _setup_perspectives(self):
709 """Add perspectives to menubar and _perspectives.
711 self._perspectives = {
712 'Default': self._c['manager'].SavePerspective(),
714 path = os.path.expanduser(self.gui.config['perspective path'])
715 if os.path.isdir(path):
716 files = sorted(os.listdir(path))
718 name, extension = os.path.splitext(fname)
719 if extension != self.gui.config['perspective extension']:
721 fpath = os.path.join(path, fname)
722 if not os.path.isfile(fpath):
725 with open(fpath, 'rU') as f:
726 perspective = f.readline()
728 self._perspectives[name] = perspective
730 selected_perspective = self.gui.config['active perspective']
731 if not self._perspectives.has_key(selected_perspective):
732 self._set_config('active perspective', 'Default')
734 self._restore_perspective(selected_perspective, force=True)
735 self._update_perspective_menu()
737 def _update_perspective_menu(self):
738 self._c['menu bar']._c['perspective'].update(
739 sorted(self._perspectives.keys()),
740 self.gui.config['active perspective'])
742 def _save_perspective(self, perspective, perspective_dir, name,
744 path = os.path.join(perspective_dir, name)
745 if extension != None:
747 if not os.path.isdir(perspective_dir):
748 os.makedirs(perspective_dir)
749 with open(path, 'w') as f:
751 self._perspectives[name] = perspective
752 self._restore_perspective(name)
753 self._update_perspective_menu()
755 def _delete_perspectives(self, perspective_dir, names,
757 self.log.debug('remove perspectives %s from %s'
758 % (names, perspective_dir))
760 path = os.path.join(perspective_dir, name)
761 if extension != None:
764 del(self._perspectives[name])
765 self._update_perspective_menu()
766 if self.gui.config['active perspective'] in names:
767 self._restore_perspective('Default')
768 # TODO: does this bug still apply?
769 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
770 # http://trac.wxwidgets.org/ticket/3258
771 # ) that makes the radio item indicator in the menu disappear.
772 # The code should be fine once this issue is fixed.
774 def _restore_perspective(self, name, force=False):
775 if name != self.gui.config['active perspective'] or force == True:
776 self.log.debug('restore perspective %s' % name)
777 self._set_config('active perspective', name)
778 self._c['manager'].LoadPerspective(self._perspectives[name])
779 self._c['manager'].Update()
780 for pane in self._c['manager'].GetAllPanes():
781 view = self._c['menu bar']._c['view']
782 if pane.name in view._c.keys():
783 view._c[pane.name].Check(pane.window.IsShown())
785 def _on_save_perspective(self, *args):
786 perspective = self._c['manager'].SavePerspective()
787 name = self.gui.config['active perspective']
788 if name == 'Default':
789 name = 'New perspective'
790 name = select_save_file(
791 directory=os.path.expanduser(self.gui.config['perspective path']),
793 extension=self.gui.config['perspective extension'],
795 message='Enter a name for the new perspective:',
796 caption='Save perspective')
799 self._save_perspective(
801 os.path.expanduser(self.gui.config['perspective path']), name=name,
802 extension=self.gui.config['perspective extension'])
804 def _on_delete_perspective(self, *args, **kwargs):
805 options = sorted([p for p in self._perspectives.keys()
807 dialog = SelectionDialog(
809 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
810 button_id=wx.ID_DELETE,
811 selection_style='multiple',
813 title='Delete perspective(s)',
814 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
815 dialog.CenterOnScreen()
817 if dialog.canceled == True:
819 names = [options[i] for i in dialog.selected]
821 self._delete_perspectives(
822 os.path.expanduser(self.gui.config['perspective path']),
823 names=names, extension=self.gui.config['perspective extension'])
825 def _on_select_perspective(self, _class, method, name):
826 self._restore_perspective(name)
829 # setup per-command versions of HookeFrame._update_curve
830 for _command in ['convert_distance_to_force',
832 'remove_cantilever_from_extension',
833 'zero_surface_contact_point',
835 setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
839 class HookeApp (wx.App):
840 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
842 Tosses up a splash screen and then loads :class:`HookeFrame` in
845 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
847 self.commands = commands
848 self.inqueue = inqueue
849 self.outqueue = outqueue
850 super(HookeApp, self).__init__(*args, **kwargs)
853 self.SetAppName('Hooke')
854 self.SetVendorName('')
855 self._setup_splash_screen()
857 height = self.gui.config['main height']
858 width = self.gui.config['main width']
859 top = self.gui.config['main top']
860 left = self.gui.config['main left']
862 # Sometimes, the ini file gets confused and sets 'left' and
863 # 'top' to large negative numbers. Here we catch and fix
864 # this. Keep small negative numbers, the user might want
873 self.gui, self.commands, self.inqueue, self.outqueue,
874 parent=None, title='Hooke',
875 pos=(left, top), size=(width, height),
876 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
878 self._c['frame'].Show(True)
879 self.SetTopWindow(self._c['frame'])
882 def _setup_splash_screen(self):
883 if self.gui.config['show splash screen'] == True:
884 path = os.path.expanduser(self.gui.config['splash screen image'])
885 if os.path.isfile(path):
886 duration = self.gui.config['splash screen duration']
888 bitmap=wx.Image(path).ConvertToBitmap(),
889 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
890 milliseconds=duration,
893 # For some reason splashDuration and sleep do not
894 # correspond to each other at least not on Windows.
895 # Maybe it's because duration is in milliseconds and
896 # sleep in seconds. Thus we need to increase the
897 # sleep time a bit. A factor of 1.2 seems to work.
899 time.sleep(sleepFactor * duration / 1000)