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.lib.evtmgr as evtmgr
37 # wxPropertyGrid is included in wxPython >= 2.9.1, see
38 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
39 # until then, we'll avoid it because of the *nix build problems.
40 #import wx.propgrid as wxpg
42 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
43 from ...engine import CommandMessage
44 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
45 from .dialog.selection import Selection as SelectionDialog
46 from .dialog.save_file import select_save_file
47 from . import menu as menu
48 from . import navbar as navbar
49 from . import panel as panel
50 from .panel.propertyeditor import props_from_argument, props_from_setting
51 from . import statusbar as statusbar
54 class HookeFrame (wx.Frame):
55 """The main Hooke-interface window.
57 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
58 super(HookeFrame, self).__init__(*args, **kwargs)
59 self.log = logging.getLogger('hooke')
61 self.commands = commands
62 self.inqueue = inqueue
63 self.outqueue = outqueue
64 self._perspectives = {} # {name: perspective_str}
68 os.path.expanduser(self.gui.config['icon image']),
72 self._c['manager'] = aui.AuiManager()
73 self._c['manager'].SetManagedWindow(self)
75 # set the gradient and drag styles
76 self._c['manager'].GetArtProvider().SetMetric(
77 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
78 self._c['manager'].SetFlags(
79 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
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._on_user_delete_playlist,
134 '_delete_playlist':self._on_delete_playlist,
135 'delete_curve':self._on_user_delete_curve,
136 '_delete_curve':self._on_delete_curve,
137 '_on_set_selected_playlist':self._on_set_selected_playlist,
138 '_on_set_selected_curve':self._on_set_selected_curve,
141 style=wx.WANTS_CHARS|wx.NO_BORDER,
142 # WANTS_CHARS so the panel doesn't eat the Return key.
145 (panel.PANELS['note'](
147 '_on_update':self._on_update_note,
150 style=wx.WANTS_CHARS|wx.NO_BORDER,
153 # ('notebook', Notebook(
155 # pos=wx.Point(client_size.x, client_size.y),
156 # size=wx.Size(430, 200),
157 # style=aui.AUI_NB_DEFAULT_STYLE
158 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
159 (panel.PANELS['commands'](
160 commands=self.commands,
161 selected=self.gui.config['selected command'],
163 'execute': self.explicit_execute_command,
164 'select_plugin': self.select_plugin,
165 'select_command': self.select_command,
166 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
169 style=wx.WANTS_CHARS|wx.NO_BORDER,
170 # WANTS_CHARS so the panel doesn't eat the Return key.
173 (panel.PANELS['propertyeditor'](
176 style=wx.WANTS_CHARS,
177 # WANTS_CHARS so the panel doesn't eat the Return key.
179 (panel.PANELS['plot'](
181 '_set_status_text': self._on_plot_status_text,
184 style=wx.WANTS_CHARS|wx.NO_BORDER,
185 # WANTS_CHARS so the panel doesn't eat the Return key.
188 (panel.PANELS['output'](
191 size=wx.Size(150, 90),
192 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
195 self._add_panel(p, style)
196 self.execute_command( # setup already loaded playlists
197 command=self._command_by_name('playlists'))
198 self.execute_command( # setup already loaded curve
199 command=self._command_by_name('get curve'))
201 def _add_panel(self, panel, style):
202 self._c[panel.name] = panel
203 m_name = panel.managed_name
204 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
205 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
208 elif style == 'center':
210 elif style == 'left':
212 elif style == 'right':
215 assert style == 'bottom', style
217 self._c['manager'].AddPane(panel, info)
219 def _setup_toolbars(self):
220 self._c['navigation bar'] = navbar.NavBar(
222 'next': self._next_curve,
223 'previous': self._previous_curve,
226 style=wx.TB_FLAT | wx.TB_NODIVIDER)
227 self._c['manager'].AddPane(
228 self._c['navigation bar'],
229 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
230 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
231 ).RightDockable(False))
233 def _bind_events(self):
234 # TODO: figure out if we can use the eventManager for menu
235 # ranges and events of 'self' without raising an assertion
237 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
238 self.Bind(wx.EVT_SIZE, self._on_size)
239 self.Bind(wx.EVT_CLOSE, self._on_close)
240 self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
242 return # TODO: cleanup
243 treeCtrl = self._c['folders'].GetTreeCtrl()
244 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
246 def _on_about(self, *args):
247 dialog = wx.MessageDialog(
249 message=self.gui._splash_text(extra_info={
250 'get-details':'click "Help -> License"'},
252 caption='About Hooke',
253 style=wx.OK|wx.ICON_INFORMATION)
257 def _on_size(self, event):
260 def _on_close(self, *args):
261 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):
276 # Panel utility functions
278 def _file_name(self, name):
279 """Cleanup names according to configured preferences.
281 if self.gui.config['hide extensions'] == True:
282 name,ext = os.path.splitext(name)
289 def _command_by_name(self, name):
290 cs = [c for c in self.commands if c.name == name]
294 raise Exception('Multiple commands named "%s"' % name)
297 def explicit_execute_command(self, _class=None, method=None,
298 command=None, args=None):
299 return self.execute_command(
300 _class=_class, method=method, command=command, args=args,
301 explicit_user_call=True)
303 def execute_command(self, _class=None, method=None,
304 command=None, args=None, explicit_user_call=False):
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(
315 args[arg.name] = value
317 # deal with counted arguments
318 if arg.name not in args:
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.
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)]
334 while (len(args[arg.name]) > 0
335 and args[arg.name][-1] == None):
337 if len(args[arg.name]) == 0:
338 args[arg.name] = arg.default
339 cm = CommandMessage(command.name, args)
340 self.gui._submit_command(
341 cm, self.inqueue, explicit_user_call=explicit_user_call)
342 # TODO: skip responses for commands that were captured by the
343 # command stack. We'd need to poll on each request, remember
344 # capture state, or add a flag to the response...
345 return self._handle_response(command_message=cm)
347 def _handle_response(self, command_message):
350 msg = self.outqueue.get()
352 if isinstance(msg, Exit):
355 elif isinstance(msg, CommandExit):
356 # TODO: display command complete
358 elif isinstance(msg, ReloadUserInterfaceConfig):
359 self.gui.reload_config(msg.config)
361 elif isinstance(msg, Request):
362 h = handler.HANDLERS[msg.type]
363 h.run(self, msg) # TODO: pause for response?
366 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
367 self._postprocess_text)
368 pp(command=command_message.command,
369 args=command_message.arguments,
373 def _handle_request(self, msg):
374 """Repeatedly try to get a response to `msg`.
377 raise NotImplementedError('_%s_request_prompt' % msg.type)
378 prompt_string = prompt(msg)
379 parser = getattr(self, '_%s_request_parser' % msg.type, None)
381 raise NotImplementedError('_%s_request_parser' % msg.type)
385 self.cmd.stdout.write(''.join([
386 error.__class__.__name__, ': ', str(error), '\n']))
387 self.cmd.stdout.write(prompt_string)
388 value = parser(msg, self.cmd.stdin.readline())
390 response = msg.response(value)
392 except ValueError, error:
394 self.inqueue.put(response)
396 def _set_config(self, option, value, section=None):
397 self.gui._set_config(section=section, option=option, value=value,
398 ui_to_command_queue=self.inqueue,
399 response_handler=self._handle_response)
402 # Command-specific postprocessing
404 def _postprocess_text(self, command, args={}, results=[]):
405 """Print the string representation of the results to the Results window.
407 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
408 approach, except that :class:`~hooke.ui.commandline.DoCommand`
409 doesn't print some internally handled messages
410 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
412 for result in results:
413 if isinstance(result, CommandExit):
414 self._c['output'].write(result.__class__.__name__+'\n')
415 self._c['output'].write(str(result).rstrip()+'\n')
417 def _postprocess_playlists(self, command, args={}, results=None):
418 """Update `self` to show the playlists.
420 if not isinstance(results[-1], Success):
421 self._postprocess_text(command, results=results)
423 assert len(results) == 2, results
424 playlists = results[0]
425 if 'playlist' in self._c:
426 for playlist in playlists:
427 if self._c['playlist'].is_playlist_loaded(playlist):
428 self._c['playlist'].update_playlist(playlist)
430 self._c['playlist'].add_playlist(playlist)
432 def _postprocess_new_playlist(self, command, args={}, results=None):
433 """Update `self` to show the new playlist.
435 if not isinstance(results[-1], Success):
436 self._postprocess_text(command, results=results)
438 assert len(results) == 2, results
439 playlist = results[0]
440 if 'playlist' in self._c:
441 loaded = self._c['playlist'].is_playlist_loaded(playlist)
442 assert loaded == False, loaded
443 self._c['playlist'].add_playlist(playlist)
445 def _postprocess_load_playlist(self, command, args={}, results=None):
446 """Update `self` to show the playlist.
448 if not isinstance(results[-1], Success):
449 self._postprocess_text(command, results=results)
451 assert len(results) == 2, results
452 playlist = results[0]
453 self._c['playlist'].add_playlist(playlist)
455 def _postprocess_get_playlist(self, command, args={}, results=[]):
456 if not isinstance(results[-1], Success):
457 self._postprocess_text(command, results=results)
459 assert len(results) == 2, results
460 playlist = results[0]
461 if 'playlist' in self._c:
462 loaded = self._c['playlist'].is_playlist_loaded(playlist)
463 assert loaded == True, loaded
464 self._c['playlist'].update_playlist(playlist)
466 def _postprocess_name_playlist(self, command, args={}, results=None):
467 """Update `self` to show the new playlist.
469 return self._postprocess_new_playlist(command, args, results)
471 def _postprocess_get_curve(self, command, args={}, results=[]):
472 """Update `self` to show the curve.
474 if not isinstance(results[-1], Success):
475 self._postprocess_text(command, results=results)
477 assert len(results) == 2, results
479 if args.get('curve', None) == None:
480 # the command defaults to the current curve of the current playlist
481 results = self.execute_command(
482 command=self._command_by_name('get playlist'))
483 playlist = results[0]
485 raise NotImplementedError()
486 if 'note' in self._c:
487 self._c['note'].set_text(curve.info.get('note', ''))
488 if 'playlist' in self._c:
489 self._c['playlist'].set_selected_curve(
491 if 'plot' in self._c:
492 self._c['plot'].set_curve(curve, config=self.gui.config)
494 def _postprocess_next_curve(self, command, args={}, results=[]):
495 """No-op. Only call 'next curve' via `self._next_curve()`.
499 def _postprocess_previous_curve(self, command, args={}, results=[]):
500 """No-op. Only call 'previous curve' via `self._previous_curve()`.
504 def _postprocess_glob_curves_to_playlist(
505 self, command, args={}, results=[]):
506 """Update `self` to show new curves.
508 if not isinstance(results[-1], Success):
509 self._postprocess_text(command, results=results)
511 if 'playlist' in self._c:
512 if args.get('playlist', None) != None:
513 playlist = args['playlist']
514 pname = playlist.name
515 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
516 assert loaded == True, loaded
517 for curve in results[:-1]:
518 self._c['playlist']._add_curve(pname, curve)
520 self.execute_command(
521 command=self._command_by_name('get playlist'))
523 def _update_curve(self, command, args={}, results=[]):
524 """Update the curve, since the available columns may have changed.
526 if isinstance(results[-1], Success):
527 self.execute_command(
528 command=self._command_by_name('get curve'))
531 # Command panel interface
533 def select_command(self, _class, method, command):
534 #self.select_plugin(plugin=command.plugin)
535 self._c['property editor'].clear()
536 self._c['property editor']._argument_from_label = {}
537 for argument in command.arguments:
538 if argument.name == 'help':
541 results = self.execute_command(
542 command=self._command_by_name('playlists'))
543 if not isinstance(results[-1], Success):
544 self._postprocess_text(command, results=results)
547 playlists = results[0]
549 results = self.execute_command(
550 command=self._command_by_name('playlist curves'))
551 if not isinstance(results[-1], Success):
552 self._postprocess_text(command, results=results)
557 ret = props_from_argument(
558 argument, curves=curves, playlists=playlists)
560 continue # property intentionally not handled (yet)
562 self._c['property editor'].append_property(p)
563 self._c['property editor']._argument_from_label[label] = (
566 self._set_config('selected command', command.name)
568 def select_plugin(self, _class=None, method=None, plugin=None):
573 # Folders panel interface
575 def _on_dir_ctrl_left_double_click(self, event):
576 file_path = self.panelFolders.GetPath()
577 if os.path.isfile(file_path):
578 if file_path.endswith('.hkp'):
579 self.do_loadlist(file_path)
584 # Note panel interface
586 def _on_update_note(self, _class, method, text):
587 """Sets the note for the active curve.
589 self.execute_command(
590 command=self._command_by_name('set note'),
595 # Playlist panel interface
597 def _on_user_delete_playlist(self, _class, method, playlist):
600 def _on_delete_playlist(self, _class, method, playlist):
601 if hasattr(playlist, 'path') and playlist.path != None:
602 os.remove(playlist.path)
604 def _on_user_delete_curve(self, _class, method, playlist, curve):
607 def _on_delete_curve(self, _class, method, playlist, curve):
608 index = playlist.index(curve)
609 results = self.execute_command(
610 command=self._command_by_name('remove curve from playlist'),
611 args={'index': index})
612 #os.remove(curve.path)
615 def _on_set_selected_playlist(self, _class, method, playlist):
616 """Call the `jump to playlist` command.
618 results = self.execute_command(
619 command=self._command_by_name('playlists'))
620 if not isinstance(results[-1], Success):
622 assert len(results) == 2, results
623 playlists = results[0]
624 matching = [p for p in playlists if p.name == playlist.name]
625 assert len(matching) == 1, matching
626 index = playlists.index(matching[0])
627 results = self.execute_command(
628 command=self._command_by_name('jump to playlist'),
629 args={'index':index})
631 def _on_set_selected_curve(self, _class, method, playlist, curve):
632 """Call the `jump to curve` command.
634 self._on_set_selected_playlist(_class, method, playlist)
635 index = playlist.index(curve)
636 results = self.execute_command(
637 command=self._command_by_name('jump to curve'),
638 args={'index':index})
639 if not isinstance(results[-1], Success):
641 #results = self.execute_command(
642 # command=self._command_by_name('get playlist'))
643 #if not isinstance(results[-1], Success):
645 self.execute_command(
646 command=self._command_by_name('get curve'))
650 # Plot panel interface
652 def _on_plot_status_text(self, _class, method, text):
653 if 'status bar' in self._c:
654 self._c['status bar'].set_plot_text(text)
660 def _next_curve(self, *args):
661 """Call the `next curve` command.
663 results = self.execute_command(
664 command=self._command_by_name('next curve'))
665 if isinstance(results[-1], Success):
666 self.execute_command(
667 command=self._command_by_name('get curve'))
669 def _previous_curve(self, *args):
670 """Call the `previous curve` command.
672 results = self.execute_command(
673 command=self._command_by_name('previous curve'))
674 if isinstance(results[-1], Success):
675 self.execute_command(
676 command=self._command_by_name('get curve'))
680 # Panel display handling
682 def _on_pane_close(self, event):
684 view = self._c['menu bar']._c['view']
685 if pane.name in view._c.keys():
686 view._c[pane.name].Check(False)
689 def _on_panel_visibility(self, _class, method, panel_name, visible):
690 pane = self._c['manager'].GetPane(panel_name)
692 #if we don't do the following, the Folders pane does not resize properly on hide/show
693 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
694 #folders_size = pane.GetSize()
695 self.panelFolders.Fit()
696 self._c['manager'].Update()
698 def _setup_perspectives(self):
699 """Add perspectives to menubar and _perspectives.
701 self._perspectives = {
702 'Default': self._c['manager'].SavePerspective(),
704 path = os.path.expanduser(self.gui.config['perspective path'])
705 if os.path.isdir(path):
706 files = sorted(os.listdir(path))
708 name, extension = os.path.splitext(fname)
709 if extension != self.gui.config['perspective extension']:
711 fpath = os.path.join(path, fname)
712 if not os.path.isfile(fpath):
715 with open(fpath, 'rU') as f:
716 perspective = f.readline()
718 self._perspectives[name] = perspective
720 selected_perspective = self.gui.config['active perspective']
721 if not self._perspectives.has_key(selected_perspective):
722 self._set_config('active perspective', 'Default')
724 self._restore_perspective(selected_perspective, force=True)
725 self._update_perspective_menu()
727 def _update_perspective_menu(self):
728 self._c['menu bar']._c['perspective'].update(
729 sorted(self._perspectives.keys()),
730 self.gui.config['active perspective'])
732 def _save_perspective(self, perspective, perspective_dir, name,
734 path = os.path.join(perspective_dir, name)
735 if extension != None:
737 if not os.path.isdir(perspective_dir):
738 os.makedirs(perspective_dir)
739 with open(path, 'w') as f:
741 self._perspectives[name] = perspective
742 self._restore_perspective(name)
743 self._update_perspective_menu()
745 def _delete_perspectives(self, perspective_dir, names,
747 self.log.debug('remove perspectives %s from %s'
748 % (names, perspective_dir))
750 path = os.path.join(perspective_dir, name)
751 if extension != None:
754 del(self._perspectives[name])
755 self._update_perspective_menu()
756 if self.gui.config['active perspective'] in names:
757 self._restore_perspective('Default')
758 # TODO: does this bug still apply?
759 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
760 # http://trac.wxwidgets.org/ticket/3258
761 # ) that makes the radio item indicator in the menu disappear.
762 # The code should be fine once this issue is fixed.
764 def _restore_perspective(self, name, force=False):
765 if name != self.gui.config['active perspective'] or force == True:
766 self.log.debug('restore perspective %s' % name)
767 self._set_config('active perspective', name)
768 self._c['manager'].LoadPerspective(self._perspectives[name])
769 self._c['manager'].Update()
770 for pane in self._c['manager'].GetAllPanes():
771 view = self._c['menu bar']._c['view']
772 if pane.name in view._c.keys():
773 view._c[pane.name].Check(pane.window.IsShown())
775 def _on_save_perspective(self, *args):
776 perspective = self._c['manager'].SavePerspective()
777 name = self.gui.config['active perspective']
778 if name == 'Default':
779 name = 'New perspective'
780 name = select_save_file(
781 directory=os.path.expanduser(self.gui.config['perspective path']),
783 extension=self.gui.config['perspective extension'],
785 message='Enter a name for the new perspective:',
786 caption='Save perspective')
789 self._save_perspective(
791 os.path.expanduser(self.gui.config['perspective path']), name=name,
792 extension=self.gui.config['perspective extension'])
794 def _on_delete_perspective(self, *args, **kwargs):
795 options = sorted([p for p in self._perspectives.keys()
797 dialog = SelectionDialog(
799 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
800 button_id=wx.ID_DELETE,
801 selection_style='multiple',
803 title='Delete perspective(s)',
804 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
805 dialog.CenterOnScreen()
807 if dialog.canceled == True:
809 names = [options[i] for i in dialog.selected]
811 self._delete_perspectives(
812 os.path.expanduser(self.gui.config['perspective path']),
813 names=names, extension=self.gui.config['perspective extension'])
815 def _on_select_perspective(self, _class, method, name):
816 self._restore_perspective(name)
819 # setup per-command versions of HookeFrame._update_curve
820 for _command in ['convert_distance_to_force',
822 'remove_cantilever_from_extension',
823 'zero_surface_contact_point',
825 setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
829 class HookeApp (wx.App):
830 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
832 Tosses up a splash screen and then loads :class:`HookeFrame` in
835 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
837 self.commands = commands
838 self.inqueue = inqueue
839 self.outqueue = outqueue
840 super(HookeApp, self).__init__(*args, **kwargs)
843 self.SetAppName('Hooke')
844 self.SetVendorName('')
845 self._setup_splash_screen()
847 height = self.gui.config['main height']
848 width = self.gui.config['main width']
849 top = self.gui.config['main top']
850 left = self.gui.config['main left']
852 # Sometimes, the ini file gets confused and sets 'left' and
853 # 'top' to large negative numbers. Here we catch and fix
854 # this. Keep small negative numbers, the user might want
863 self.gui, self.commands, self.inqueue, self.outqueue,
864 parent=None, title='Hooke',
865 pos=(left, top), size=(width, height),
866 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
868 self._c['frame'].Show(True)
869 self.SetTopWindow(self._c['frame'])
872 def _setup_splash_screen(self):
873 if self.gui.config['show splash screen'] == True:
874 path = os.path.expanduser(self.gui.config['splash screen image'])
875 if os.path.isfile(path):
876 duration = self.gui.config['splash screen duration']
878 bitmap=wx.Image(path).ConvertToBitmap(),
879 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
880 milliseconds=duration,
883 # For some reason splashDuration and sleep do not
884 # correspond to each other at least not on Windows.
885 # Maybe it's because duration is in milliseconds and
886 # sleep in seconds. Thus we need to increase the
887 # sleep time a bit. A factor of 1.2 seems to work.
889 time.sleep(sleepFactor * duration / 1000)