1 # Copyright (C) 2010-2011 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 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
26 wxversion.select(WX_GOOD)
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 ...config import Setting
46 from ...engine import CommandMessage
47 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
48 from ...ui import UserInterface
49 from .dialog.selection import Selection as SelectionDialog
50 from .dialog.save_file import select_save_file
51 from . import menu as menu
52 from . import navbar as navbar
53 from . import panel as panel
54 from .panel.propertyeditor import props_from_argument, props_from_setting
55 from . import statusbar as statusbar
58 class HookeFrame (wx.Frame):
59 """The main Hooke-interface window.
61 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
62 super(HookeFrame, self).__init__(*args, **kwargs)
63 self.log = logging.getLogger('hooke')
65 self.commands = commands
66 self.inqueue = inqueue
67 self.outqueue = outqueue
68 self._perspectives = {} # {name: perspective_str}
72 os.path.expanduser(self.gui.config['icon image']),
76 self._c['manager'] = aui.AuiManager()
77 self._c['manager'].SetManagedWindow(self)
79 # set the gradient and drag styles
80 self._c['manager'].GetArtProvider().SetMetric(
81 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
82 self._c['manager'].SetFlags(
83 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
85 # Min size for the frame itself isn't completely done. See
86 # the end of FrameManager::Update() for the test code. For
87 # now, just hard code a frame minimum size.
88 #self.SetMinSize(wx.Size(500, 500))
91 self._setup_toolbars()
92 self._c['manager'].Update() # commit pending changes
94 # Create the menubar after the panes so that the default
95 # perspective is created with all panes open
96 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
97 self._c['menu bar'] = menu.HookeMenuBar(
101 'close': self._on_close,
102 'about': self._on_about,
103 'view_panel': self._on_panel_visibility,
104 'save_perspective': self._on_save_perspective,
105 'delete_perspective': self._on_delete_perspective,
106 'select_perspective': self._on_select_perspective,
108 self.SetMenuBar(self._c['menu bar'])
110 self._c['status bar'] = statusbar.StatusBar(
112 style=wx.ST_SIZEGRIP)
113 self.SetStatusBar(self._c['status bar'])
115 self._setup_perspectives()
117 return # TODO: cleanup
118 self._displayed_plot = None
119 #load default list, if possible
120 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
125 def _setup_panels(self):
126 client_size = self.GetClientSize()
128 # ('folders', wx.GenericDirCtrl(
130 # dir=self.gui.config['folders-workdir'],
132 # style=wx.DIRCTRL_SHOW_FILTERS,
133 # filter=self.gui.config['folders-filters'],
134 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
135 (panel.PANELS['playlist'](
137 'delete_playlist':self._on_user_delete_playlist,
138 '_delete_playlist':self._on_delete_playlist,
139 'delete_curve':self._on_user_delete_curve,
140 '_delete_curve':self._on_delete_curve,
141 '_on_set_selected_playlist':self._on_set_selected_playlist,
142 '_on_set_selected_curve':self._on_set_selected_curve,
145 style=wx.WANTS_CHARS|wx.NO_BORDER,
146 # WANTS_CHARS so the panel doesn't eat the Return key.
149 (panel.PANELS['note'](
151 '_on_update':self._on_update_note,
154 style=wx.WANTS_CHARS|wx.NO_BORDER,
157 # ('notebook', Notebook(
159 # pos=wx.Point(client_size.x, client_size.y),
160 # size=wx.Size(430, 200),
161 # style=aui.AUI_NB_DEFAULT_STYLE
162 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
163 (panel.PANELS['commands'](
164 commands=self.commands,
165 selected=self.gui.config['selected command'],
167 'execute': self.explicit_execute_command,
168 'select_plugin': self.select_plugin,
169 'select_command': self.select_command,
170 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
173 style=wx.WANTS_CHARS|wx.NO_BORDER,
174 # WANTS_CHARS so the panel doesn't eat the Return key.
177 (panel.PANELS['propertyeditor'](
180 style=wx.WANTS_CHARS,
181 # WANTS_CHARS so the panel doesn't eat the Return key.
183 (panel.PANELS['plot'](
185 '_set_status_text': self._on_plot_status_text,
188 style=wx.WANTS_CHARS|wx.NO_BORDER,
189 # WANTS_CHARS so the panel doesn't eat the Return key.
192 (panel.PANELS['output'](
195 size=wx.Size(150, 90),
196 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
199 self._add_panel(p, style)
200 self.execute_command( # setup already loaded playlists
201 command=self._command_by_name('playlists'))
202 self.execute_command( # setup already loaded curve
203 command=self._command_by_name('get curve'))
205 def _add_panel(self, panel, style):
206 self._c[panel.name] = panel
207 m_name = panel.managed_name
208 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
209 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
212 elif style == 'center':
214 elif style == 'left':
216 elif style == 'right':
219 assert style == 'bottom', style
221 self._c['manager'].AddPane(panel, info)
223 def _setup_toolbars(self):
224 self._c['navigation bar'] = navbar.NavBar(
226 'next': self._next_curve,
227 'previous': self._previous_curve,
230 style=wx.TB_FLAT | wx.TB_NODIVIDER)
231 self._c['manager'].AddPane(
232 self._c['navigation bar'],
233 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
234 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
235 ).RightDockable(False))
237 def _bind_events(self):
238 # TODO: figure out if we can use the eventManager for menu
239 # ranges and events of 'self' without raising an assertion
241 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
242 self.Bind(wx.EVT_SIZE, self._on_size)
243 self.Bind(wx.EVT_CLOSE, self._on_close)
244 self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
246 return # TODO: cleanup
247 treeCtrl = self._c['folders'].GetTreeCtrl()
248 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
250 def _on_about(self, *args):
251 dialog = wx.MessageDialog(
253 message=self.gui._splash_text(extra_info={
254 'get-details':'click "Help -> License"'},
256 caption='About Hooke',
257 style=wx.OK|wx.ICON_INFORMATION)
261 def _on_size(self, event):
264 def _on_close(self, *args):
265 self.log.info('closing GUI framework')
267 self._set_config('main height', self.GetSize().GetHeight())
268 self._set_config('main left', self.GetPosition()[0])
269 self._set_config('main top', self.GetPosition()[1])
270 self._set_config('main width', self.GetSize().GetWidth())
271 self._c['manager'].UnInit()
272 del self._c['manager']
275 def _on_erase_background(self, event):
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 _update_curve(self, command, args={}, results=[]):
528 """Update the curve, since the available columns may have changed.
530 if isinstance(results[-1], Success):
531 self.execute_command(
532 command=self._command_by_name('get curve'))
535 # Command panel interface
537 def select_command(self, _class, method, command):
538 #self.select_plugin(plugin=command.plugin)
539 self._c['property editor'].clear()
540 self._c['property editor']._argument_from_label = {}
541 for argument in command.arguments:
542 if argument.name == 'help':
545 results = self.execute_command(
546 command=self._command_by_name('playlists'))
547 if not isinstance(results[-1], Success):
548 self._postprocess_text(command, results=results)
551 playlists = results[0]
553 results = self.execute_command(
554 command=self._command_by_name('playlist curves'))
555 if not isinstance(results[-1], Success):
556 self._postprocess_text(command, results=results)
561 ret = props_from_argument(
562 argument, curves=curves, playlists=playlists)
564 continue # property intentionally not handled (yet)
566 self._c['property editor'].append_property(p)
567 self._c['property editor']._argument_from_label[label] = (
570 self._set_config('selected command', command.name)
572 def select_plugin(self, _class=None, method=None, plugin=None):
577 # Folders panel interface
579 def _on_dir_ctrl_left_double_click(self, event):
580 file_path = self.panelFolders.GetPath()
581 if os.path.isfile(file_path):
582 if file_path.endswith('.hkp'):
583 self.do_loadlist(file_path)
588 # Note panel interface
590 def _on_update_note(self, _class, method, text):
591 """Sets the note for the active curve.
593 self.execute_command(
594 command=self._command_by_name('set note'),
599 # Playlist panel interface
601 def _on_user_delete_playlist(self, _class, method, playlist):
604 def _on_delete_playlist(self, _class, method, playlist):
605 if hasattr(playlist, 'path') and playlist.path != None:
606 os.remove(playlist.path)
608 def _on_user_delete_curve(self, _class, method, playlist, curve):
611 def _on_delete_curve(self, _class, method, playlist, curve):
612 # TODO: execute_command 'remove curve from playlist'
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)
893 class GUI (UserInterface):
894 """wxWindows graphical user interface.
897 super(GUI, self).__init__(name='gui')
899 def default_settings(self):
900 """Return a list of :class:`hooke.config.Setting`\s for any
901 configurable UI settings.
903 The suggested section setting is::
905 Setting(section=self.setting_section, help=self.__doc__)
908 Setting(section=self.setting_section, help=self.__doc__),
909 Setting(section=self.setting_section, option='icon image',
910 value=os.path.join('doc', 'img', 'microscope.ico'),
912 help='Path to the hooke icon image.'),
913 Setting(section=self.setting_section, option='show splash screen',
914 value=True, type='bool',
915 help='Enable/disable the splash screen'),
916 Setting(section=self.setting_section, option='splash screen image',
917 value=os.path.join('doc', 'img', 'hooke.jpg'),
919 help='Path to the Hooke splash screen image.'),
920 Setting(section=self.setting_section,
921 option='splash screen duration',
922 value=1000, type='int',
923 help='Duration of the splash screen in milliseconds.'),
924 Setting(section=self.setting_section, option='perspective path',
925 value=os.path.join('resources', 'gui', 'perspective'),
926 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
927 Setting(section=self.setting_section, option='perspective extension',
929 help='Extension for perspective files.'),
930 Setting(section=self.setting_section, option='hide extensions',
931 value=False, type='bool',
932 help='Hide file extensions when displaying names.'),
933 Setting(section=self.setting_section, option='plot legend',
934 value=True, type='bool',
935 help='Enable/disable the plot legend.'),
936 Setting(section=self.setting_section, option='plot SI format',
937 value='True', type='bool',
938 help='Enable/disable SI plot axes numbering.'),
939 Setting(section=self.setting_section, option='plot decimals',
941 help='Number of decimal places to show if "plot SI format" is enabled.'),
942 Setting(section=self.setting_section, option='folders-workdir',
943 value='.', type='path',
944 help='This should probably go...'),
945 Setting(section=self.setting_section, option='folders-filters',
946 value='.', type='path',
947 help='This should probably go...'),
948 Setting(section=self.setting_section, option='active perspective',
950 help='Name of active perspective file (or "Default").'),
951 Setting(section=self.setting_section,
952 option='folders-filter-index',
954 help='This should probably go...'),
955 Setting(section=self.setting_section, option='main height',
956 value=450, type='int',
957 help='Height of main window in pixels.'),
958 Setting(section=self.setting_section, option='main width',
959 value=800, type='int',
960 help='Width of main window in pixels.'),
961 Setting(section=self.setting_section, option='main top',
963 help='Pixels from screen top to top of main window.'),
964 Setting(section=self.setting_section, option='main left',
966 help='Pixels from screen left to left of main window.'),
967 Setting(section=self.setting_section, option='selected command',
968 value='load playlist',
969 help='Name of the initially selected command.'),
972 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
976 app = HookeApp(gui=self,
978 inqueue=ui_to_command_queue,
979 outqueue=command_to_ui_queue,
983 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
984 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)