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
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.
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.
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/>.
19 """Define :class:`HookeApp` and related, central application classes.
25 wxversion.select(WX_GOOD)
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
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
55 class HookeFrame (wx.Frame):
56 """The main Hooke-interface window.
58 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
59 super(HookeFrame, self).__init__(*args, **kwargs)
60 self.log = logging.getLogger('hooke')
62 self.commands = commands
63 self.inqueue = inqueue
64 self.outqueue = outqueue
65 self._perspectives = {} # {name: perspective_str}
69 os.path.expanduser(self.gui.config['icon image']),
73 self._c['manager'] = aui.AuiManager()
74 self._c['manager'].SetManagedWindow(self)
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)
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))
88 self._setup_toolbars()
89 self._c['manager'].Update() # commit pending changes
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(
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,
105 self.SetMenuBar(self._c['menu bar'])
107 self._c['status bar'] = statusbar.StatusBar(
109 style=wx.ST_SIZEGRIP)
110 self.SetStatusBar(self._c['status bar'])
112 self._setup_perspectives()
114 return # TODO: cleanup
115 self._displayed_plot = None
116 #load default list, if possible
117 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
122 def _setup_panels(self):
123 client_size = self.GetClientSize()
125 # ('folders', wx.GenericDirCtrl(
127 # dir=self.gui.config['folders-workdir'],
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'](
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,
142 style=wx.WANTS_CHARS|wx.NO_BORDER,
143 # WANTS_CHARS so the panel doesn't eat the Return key.
146 (panel.PANELS['note'](
148 '_on_update':self._on_update_note,
151 style=wx.WANTS_CHARS|wx.NO_BORDER,
154 # ('notebook', Notebook(
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'],
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,
170 style=wx.WANTS_CHARS|wx.NO_BORDER,
171 # WANTS_CHARS so the panel doesn't eat the Return key.
174 (panel.PANELS['propertyeditor'](
177 style=wx.WANTS_CHARS,
178 # WANTS_CHARS so the panel doesn't eat the Return key.
180 (panel.PANELS['plot'](
182 '_set_status_text': self._on_plot_status_text,
185 style=wx.WANTS_CHARS|wx.NO_BORDER,
186 # WANTS_CHARS so the panel doesn't eat the Return key.
189 (panel.PANELS['output'](
192 size=wx.Size(150, 90),
193 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
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'))
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)
209 elif style == 'center':
211 elif style == 'left':
213 elif style == 'right':
216 assert style == 'bottom', style
218 self._c['manager'].AddPane(panel, info)
220 def _setup_toolbars(self):
221 self._c['navigation bar'] = navbar.NavBar(
223 'next': self._next_curve,
224 'previous': self._previous_curve,
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))
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
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)
243 return # TODO: cleanup
244 treeCtrl = self._c['folders'].GetTreeCtrl()
245 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
247 def _on_about(self, *args):
248 dialog = wx.MessageDialog(
250 message=self.gui._splash_text(extra_info={
251 'get-details':'click "Help -> License"'},
253 caption='About Hooke',
254 style=wx.OK|wx.ICON_INFORMATION)
258 def _on_size(self, event):
261 def _on_close(self, *args):
262 self.log.info('closing GUI framework')
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']
272 def _on_erase_background(self, event):
277 # Panel utility functions
279 def _file_name(self, name):
280 """Cleanup names according to configured preferences.
282 if self.gui.config['hide extensions'] == True:
283 name,ext = os.path.splitext(name)
290 def _command_by_name(self, name):
291 cs = [c for c in self.commands if c.name == name]
295 raise Exception('Multiple commands named "%s"' % name)
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)
304 def execute_command(self, _class=None, method=None,
305 command=None, args=None, explicit_user_call=False):
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(
316 args[arg.name] = value
318 # deal with counted arguments
319 if arg.name not in args:
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.
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)]
335 while (len(args[arg.name]) > 0
336 and args[arg.name][-1] == None):
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)
348 def _handle_response(self, command_message):
351 msg = self.outqueue.get()
353 if isinstance(msg, Exit):
356 elif isinstance(msg, CommandExit):
357 # TODO: display command complete
359 elif isinstance(msg, ReloadUserInterfaceConfig):
360 self.gui.reload_config(msg.config)
362 elif isinstance(msg, Request):
363 h = handler.HANDLERS[msg.type]
364 h.run(self, msg) # TODO: pause for response?
367 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
368 self._postprocess_text)
369 pp(command=command_message.command,
370 args=command_message.arguments,
374 def _handle_request(self, msg):
375 """Repeatedly try to get a response to `msg`.
378 raise NotImplementedError('_%s_request_prompt' % msg.type)
379 prompt_string = prompt(msg)
380 parser = getattr(self, '_%s_request_parser' % msg.type, None)
382 raise NotImplementedError('_%s_request_parser' % msg.type)
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())
391 response = msg.response(value)
393 except ValueError, error:
395 self.inqueue.put(response)
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)
403 # Command-specific postprocessing
405 def _postprocess_text(self, command, args={}, results=[]):
406 """Print the string representation of the results to the Results window.
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`).
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')
418 def _postprocess_playlists(self, command, args={}, results=None):
419 """Update `self` to show the playlists.
421 if not isinstance(results[-1], Success):
422 self._postprocess_text(command, results=results)
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)
431 self._c['playlist'].add_playlist(playlist)
433 def _postprocess_new_playlist(self, command, args={}, results=None):
434 """Update `self` to show the new playlist.
436 if not isinstance(results[-1], Success):
437 self._postprocess_text(command, results=results)
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)
446 def _postprocess_load_playlist(self, command, args={}, results=None):
447 """Update `self` to show the playlist.
449 if not isinstance(results[-1], Success):
450 self._postprocess_text(command, results=results)
452 assert len(results) == 2, results
453 playlist = results[0]
454 self._c['playlist'].add_playlist(playlist)
456 def _postprocess_get_playlist(self, command, args={}, results=[]):
457 if not isinstance(results[-1], Success):
458 self._postprocess_text(command, results=results)
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)
467 def _postprocess_name_playlist(self, command, args={}, results=None):
468 """Update `self` to show the new playlist.
470 return self._postprocess_new_playlist(command, args, results)
472 def _postprocess_get_curve(self, command, args={}, results=[]):
473 """Update `self` to show the curve.
475 if not isinstance(results[-1], Success):
476 self._postprocess_text(command, results=results)
478 assert len(results) == 2, results
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]
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(
492 if 'plot' in self._c:
493 self._c['plot'].set_curve(curve, config=self.gui.config)
495 def _postprocess_next_curve(self, command, args={}, results=[]):
496 """No-op. Only call 'next curve' via `self._next_curve()`.
500 def _postprocess_previous_curve(self, command, args={}, results=[]):
501 """No-op. Only call 'previous curve' via `self._previous_curve()`.
505 def _postprocess_glob_curves_to_playlist(
506 self, command, args={}, results=[]):
507 """Update `self` to show new curves.
509 if not isinstance(results[-1], Success):
510 self._postprocess_text(command, results=results)
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)
521 self.execute_command(
522 command=self._command_by_name('get playlist'))
524 def _update_curve(self, command, args={}, results=[]):
525 """Update the curve, since the available columns may have changed.
527 if isinstance(results[-1], Success):
528 self.execute_command(
529 command=self._command_by_name('get curve'))
532 # Command panel interface
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':
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)
548 playlists = results[0]
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)
558 ret = props_from_argument(
559 argument, curves=curves, playlists=playlists)
561 continue # property intentionally not handled (yet)
563 self._c['property editor'].append_property(p)
564 self._c['property editor']._argument_from_label[label] = (
567 self._set_config('selected command', command.name)
569 def select_plugin(self, _class=None, method=None, plugin=None):
574 # Folders panel interface
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)
585 # Note panel interface
587 def _on_update_note(self, _class, method, text):
588 """Sets the note for the active curve.
590 self.execute_command(
591 command=self._command_by_name('set note'),
596 # Playlist panel interface
598 def _on_user_delete_playlist(self, _class, method, playlist):
601 def _on_delete_playlist(self, _class, method, playlist):
602 if hasattr(playlist, 'path') and playlist.path != None:
603 os.remove(playlist.path)
605 def _on_user_delete_curve(self, _class, method, playlist, curve):
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)
616 def _on_set_selected_playlist(self, _class, method, playlist):
617 """Call the `jump to playlist` command.
619 results = self.execute_command(
620 command=self._command_by_name('playlists'))
621 if not isinstance(results[-1], Success):
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})
632 def _on_set_selected_curve(self, _class, method, playlist, curve):
633 """Call the `jump to curve` command.
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):
642 #results = self.execute_command(
643 # command=self._command_by_name('get playlist'))
644 #if not isinstance(results[-1], Success):
646 self.execute_command(
647 command=self._command_by_name('get curve'))
651 # Plot panel interface
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)
661 def _next_curve(self, *args):
662 """Call the `next curve` command.
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'))
670 def _previous_curve(self, *args):
671 """Call the `previous curve` command.
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'))
681 # Panel display handling
683 def _on_pane_close(self, event):
685 view = self._c['menu bar']._c['view']
686 if pane.name in view._c.keys():
687 view._c[pane.name].Check(False)
690 def _on_panel_visibility(self, _class, method, panel_name, visible):
691 pane = self._c['manager'].GetPane(panel_name)
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()
699 def _setup_perspectives(self):
700 """Add perspectives to menubar and _perspectives.
702 self._perspectives = {
703 'Default': self._c['manager'].SavePerspective(),
705 path = os.path.expanduser(self.gui.config['perspective path'])
706 if os.path.isdir(path):
707 files = sorted(os.listdir(path))
709 name, extension = os.path.splitext(fname)
710 if extension != self.gui.config['perspective extension']:
712 fpath = os.path.join(path, fname)
713 if not os.path.isfile(fpath):
716 with open(fpath, 'rU') as f:
717 perspective = f.readline()
719 self._perspectives[name] = perspective
721 selected_perspective = self.gui.config['active perspective']
722 if not self._perspectives.has_key(selected_perspective):
723 self._set_config('active perspective', 'Default')
725 self._restore_perspective(selected_perspective, force=True)
726 self._update_perspective_menu()
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'])
733 def _save_perspective(self, perspective, perspective_dir, name,
735 path = os.path.join(perspective_dir, name)
736 if extension != None:
738 if not os.path.isdir(perspective_dir):
739 os.makedirs(perspective_dir)
740 with open(path, 'w') as f:
742 self._perspectives[name] = perspective
743 self._restore_perspective(name)
744 self._update_perspective_menu()
746 def _delete_perspectives(self, perspective_dir, names,
748 self.log.debug('remove perspectives %s from %s'
749 % (names, perspective_dir))
751 path = os.path.join(perspective_dir, name)
752 if extension != None:
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.
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())
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']),
784 extension=self.gui.config['perspective extension'],
786 message='Enter a name for the new perspective:',
787 caption='Save perspective')
790 self._save_perspective(
792 os.path.expanduser(self.gui.config['perspective path']), name=name,
793 extension=self.gui.config['perspective extension'])
795 def _on_delete_perspective(self, *args, **kwargs):
796 options = sorted([p for p in self._perspectives.keys()
798 dialog = SelectionDialog(
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',
804 title='Delete perspective(s)',
805 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
806 dialog.CenterOnScreen()
808 if dialog.canceled == True:
810 names = [options[i] for i in dialog.selected]
812 self._delete_perspectives(
813 os.path.expanduser(self.gui.config['perspective path']),
814 names=names, extension=self.gui.config['perspective extension'])
816 def _on_select_perspective(self, _class, method, name):
817 self._restore_perspective(name)
820 # setup per-command versions of HookeFrame._update_curve
821 for _command in ['convert_distance_to_force',
823 'remove_cantilever_from_extension',
824 'zero_surface_contact_point',
826 setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
830 class HookeApp (wx.App):
831 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
833 Tosses up a splash screen and then loads :class:`HookeFrame` in
836 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
838 self.commands = commands
839 self.inqueue = inqueue
840 self.outqueue = outqueue
841 super(HookeApp, self).__init__(*args, **kwargs)
844 self.SetAppName('Hooke')
845 self.SetVendorName('')
846 self._setup_splash_screen()
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']
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
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),
869 self._c['frame'].Show(True)
870 self.SetTopWindow(self._c['frame'])
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']
879 bitmap=wx.Image(path).ConvertToBitmap(),
880 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
881 milliseconds=duration,
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.
890 time.sleep(sleepFactor * duration / 1000)