1 # Copyright (C) 2008-2010 Fabrizio Benedetti
2 # Massimo Sandal <devicerandom@gmail.com>
3 # Rolf Schmidt <rschmidt@alcor.concordia.ca>
4 # W. Trevor King <wking@drexel.edu>
6 # This file is part of Hooke.
8 # Hooke is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU Lesser General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
13 # Hooke is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
16 # Public License for more details.
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with Hooke. If not, see
20 # <http://www.gnu.org/licenses/>.
22 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
29 wxversion.select(WX_GOOD)
41 import wx.lib.evtmgr as evtmgr
42 # wxPropertyGrid is included in wxPython >= 2.9.1, see
43 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
44 # until then, we'll avoid it because of the *nix build problems.
45 #import wx.propgrid as wxpg
47 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
48 from ...config import Setting
49 from ...engine import CommandMessage
50 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
51 from ...ui import UserInterface
52 from .dialog.selection import Selection as SelectionDialog
53 from .dialog.save_file import select_save_file
54 from . import menu as menu
55 from . import navbar as navbar
56 from . import panel as panel
57 from .panel.propertyeditor import props_from_argument, props_from_setting
58 from . import statusbar as statusbar
61 class HookeFrame (wx.Frame):
62 """The main Hooke-interface window.
64 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
65 super(HookeFrame, self).__init__(*args, **kwargs)
66 self.log = logging.getLogger('hooke')
68 self.commands = commands
69 self.inqueue = inqueue
70 self.outqueue = outqueue
71 self._perspectives = {} # {name: perspective_str}
75 os.path.expanduser(self.gui.config['icon image']),
79 self._c['manager'] = aui.AuiManager()
80 self._c['manager'].SetManagedWindow(self)
82 # set the gradient and drag styles
83 self._c['manager'].GetArtProvider().SetMetric(
84 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
85 self._c['manager'].SetFlags(
86 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
88 # Min size for the frame itself isn't completely done. See
89 # the end of FrameManager::Update() for the test code. For
90 # now, just hard code a frame minimum size.
91 #self.SetMinSize(wx.Size(500, 500))
94 self._setup_toolbars()
95 self._c['manager'].Update() # commit pending changes
97 # Create the menubar after the panes so that the default
98 # perspective is created with all panes open
99 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
100 self._c['menu bar'] = menu.HookeMenuBar(
104 'close': self._on_close,
105 'about': self._on_about,
106 'view_panel': self._on_panel_visibility,
107 'save_perspective': self._on_save_perspective,
108 'delete_perspective': self._on_delete_perspective,
109 'select_perspective': self._on_select_perspective,
111 self.SetMenuBar(self._c['menu bar'])
113 self._c['status bar'] = statusbar.StatusBar(
115 style=wx.ST_SIZEGRIP)
116 self.SetStatusBar(self._c['status bar'])
118 self._setup_perspectives()
120 return # TODO: cleanup
121 self._displayed_plot = None
122 #load default list, if possible
123 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
128 def _setup_panels(self):
129 client_size = self.GetClientSize()
131 # ('folders', wx.GenericDirCtrl(
133 # dir=self.gui.config['folders-workdir'],
135 # style=wx.DIRCTRL_SHOW_FILTERS,
136 # filter=self.gui.config['folders-filters'],
137 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
138 (panel.PANELS['playlist'](
140 'delete_playlist':self._on_user_delete_playlist,
141 '_delete_playlist':self._on_delete_playlist,
142 'delete_curve':self._on_user_delete_curve,
143 '_delete_curve':self._on_delete_curve,
144 '_on_set_selected_playlist':self._on_set_selected_playlist,
145 '_on_set_selected_curve':self._on_set_selected_curve,
148 style=wx.WANTS_CHARS|wx.NO_BORDER,
149 # WANTS_CHARS so the panel doesn't eat the Return key.
152 (panel.PANELS['note'](
154 '_on_update':self._on_update_note,
157 style=wx.WANTS_CHARS|wx.NO_BORDER,
160 # ('notebook', Notebook(
162 # pos=wx.Point(client_size.x, client_size.y),
163 # size=wx.Size(430, 200),
164 # style=aui.AUI_NB_DEFAULT_STYLE
165 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
166 (panel.PANELS['commands'](
167 commands=self.commands,
168 selected=self.gui.config['selected command'],
170 'execute': self.explicit_execute_command,
171 'select_plugin': self.select_plugin,
172 'select_command': self.select_command,
173 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
176 style=wx.WANTS_CHARS|wx.NO_BORDER,
177 # WANTS_CHARS so the panel doesn't eat the Return key.
180 (panel.PANELS['propertyeditor'](
183 style=wx.WANTS_CHARS,
184 # WANTS_CHARS so the panel doesn't eat the Return key.
186 (panel.PANELS['plot'](
188 '_set_status_text': self._on_plot_status_text,
191 style=wx.WANTS_CHARS|wx.NO_BORDER,
192 # WANTS_CHARS so the panel doesn't eat the Return key.
195 (panel.PANELS['output'](
198 size=wx.Size(150, 90),
199 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
202 self._add_panel(p, style)
203 self.execute_command( # setup already loaded playlists
204 command=self._command_by_name('playlists'))
205 self.execute_command( # setup already loaded curve
206 command=self._command_by_name('get curve'))
208 def _add_panel(self, panel, style):
209 self._c[panel.name] = panel
210 m_name = panel.managed_name
211 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
212 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
215 elif style == 'center':
217 elif style == 'left':
219 elif style == 'right':
222 assert style == 'bottom', style
224 self._c['manager'].AddPane(panel, info)
226 def _setup_toolbars(self):
227 self._c['navigation bar'] = navbar.NavBar(
229 'next': self._next_curve,
230 'previous': self._previous_curve,
233 style=wx.TB_FLAT | wx.TB_NODIVIDER)
234 self._c['manager'].AddPane(
235 self._c['navigation bar'],
236 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
237 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
238 ).RightDockable(False))
240 def _bind_events(self):
241 # TODO: figure out if we can use the eventManager for menu
242 # ranges and events of 'self' without raising an assertion
244 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
245 self.Bind(wx.EVT_SIZE, self._on_size)
246 self.Bind(wx.EVT_CLOSE, self._on_close)
247 self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
249 return # TODO: cleanup
250 treeCtrl = self._c['folders'].GetTreeCtrl()
251 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
253 def _on_about(self, *args):
254 dialog = wx.MessageDialog(
256 message=self.gui._splash_text(extra_info={
257 'get-details':'click "Help -> License"'},
259 caption='About Hooke',
260 style=wx.OK|wx.ICON_INFORMATION)
264 def _on_size(self, event):
267 def _on_close(self, *args):
268 self.log.info('closing GUI framework')
270 self._set_config('main height', self.GetSize().GetHeight())
271 self._set_config('main left', self.GetPosition()[0])
272 self._set_config('main top', self.GetPosition()[1])
273 self._set_config('main width', self.GetSize().GetWidth())
274 self._c['manager'].UnInit()
275 del self._c['manager']
278 def _on_erase_background(self, event):
283 # Panel utility functions
285 def _file_name(self, name):
286 """Cleanup names according to configured preferences.
288 if self.gui.config['hide extensions'] == True:
289 name,ext = os.path.splitext(name)
296 def _command_by_name(self, name):
297 cs = [c for c in self.commands if c.name == name]
301 raise Exception('Multiple commands named "%s"' % name)
304 def explicit_execute_command(self, _class=None, method=None,
305 command=None, args=None):
306 return self.execute_command(
307 _class=_class, method=method, command=command, args=args,
308 explicit_user_call=True)
310 def execute_command(self, _class=None, method=None,
311 command=None, args=None, explicit_user_call=False):
314 if ('property editor' in self._c
315 and self.gui.config['selected command'] == command.name):
316 for name,value in self._c['property editor'].get_values().items():
317 arg = self._c['property editor']._argument_from_label.get(
322 args[arg.name] = value
324 # deal with counted arguments
325 if arg.name not in args:
327 index = int(name[len(arg.name):])
328 args[arg.name][index] = value
329 for arg in command.arguments:
330 if arg.name not in args:
331 continue # undisplayed argument, e.g. 'driver' types.
333 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
334 count = arg._display_count
335 if count != 1 and arg.name in args:
336 keys = sorted(args[arg.name].keys())
337 assert keys == range(count), keys
338 args[arg.name] = [args[arg.name][i]
339 for i in range(count)]
341 while (len(args[arg.name]) > 0
342 and args[arg.name][-1] == None):
344 if len(args[arg.name]) == 0:
345 args[arg.name] = arg.default
346 cm = CommandMessage(command.name, args)
347 self.gui._submit_command(
348 cm, self.inqueue, explicit_user_call=explicit_user_call)
349 # TODO: skip responses for commands that were captured by the
350 # command stack. We'd need to poll on each request, remember
351 # capture state, or add a flag to the response...
352 return self._handle_response(command_message=cm)
354 def _handle_response(self, command_message):
357 msg = self.outqueue.get()
359 if isinstance(msg, Exit):
362 elif isinstance(msg, CommandExit):
363 # TODO: display command complete
365 elif isinstance(msg, ReloadUserInterfaceConfig):
366 self.gui.reload_config(msg.config)
368 elif isinstance(msg, Request):
369 h = handler.HANDLERS[msg.type]
370 h.run(self, msg) # TODO: pause for response?
373 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
374 self._postprocess_text)
375 pp(command=command_message.command,
376 args=command_message.arguments,
380 def _handle_request(self, msg):
381 """Repeatedly try to get a response to `msg`.
384 raise NotImplementedError('_%s_request_prompt' % msg.type)
385 prompt_string = prompt(msg)
386 parser = getattr(self, '_%s_request_parser' % msg.type, None)
388 raise NotImplementedError('_%s_request_parser' % msg.type)
392 self.cmd.stdout.write(''.join([
393 error.__class__.__name__, ': ', str(error), '\n']))
394 self.cmd.stdout.write(prompt_string)
395 value = parser(msg, self.cmd.stdin.readline())
397 response = msg.response(value)
399 except ValueError, error:
401 self.inqueue.put(response)
403 def _set_config(self, option, value, section=None):
404 self.gui._set_config(section=section, option=option, value=value,
405 ui_to_command_queue=self.inqueue,
406 response_handler=self._handle_response)
409 # Command-specific postprocessing
411 def _postprocess_text(self, command, args={}, results=[]):
412 """Print the string representation of the results to the Results window.
414 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
415 approach, except that :class:`~hooke.ui.commandline.DoCommand`
416 doesn't print some internally handled messages
417 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
419 for result in results:
420 if isinstance(result, CommandExit):
421 self._c['output'].write(result.__class__.__name__+'\n')
422 self._c['output'].write(str(result).rstrip()+'\n')
424 def _postprocess_playlists(self, command, args={}, results=None):
425 """Update `self` to show the playlists.
427 if not isinstance(results[-1], Success):
428 self._postprocess_text(command, results=results)
430 assert len(results) == 2, results
431 playlists = results[0]
432 if 'playlist' in self._c:
433 for playlist in playlists:
434 if self._c['playlist'].is_playlist_loaded(playlist):
435 self._c['playlist'].update_playlist(playlist)
437 self._c['playlist'].add_playlist(playlist)
439 def _postprocess_new_playlist(self, command, args={}, results=None):
440 """Update `self` to show the new playlist.
442 if not isinstance(results[-1], Success):
443 self._postprocess_text(command, results=results)
445 assert len(results) == 2, results
446 playlist = results[0]
447 if 'playlist' in self._c:
448 loaded = self._c['playlist'].is_playlist_loaded(playlist)
449 assert loaded == False, loaded
450 self._c['playlist'].add_playlist(playlist)
452 def _postprocess_load_playlist(self, command, args={}, results=None):
453 """Update `self` to show the playlist.
455 if not isinstance(results[-1], Success):
456 self._postprocess_text(command, results=results)
458 assert len(results) == 2, results
459 playlist = results[0]
460 self._c['playlist'].add_playlist(playlist)
462 def _postprocess_get_playlist(self, command, args={}, results=[]):
463 if not isinstance(results[-1], Success):
464 self._postprocess_text(command, results=results)
466 assert len(results) == 2, results
467 playlist = results[0]
468 if 'playlist' in self._c:
469 loaded = self._c['playlist'].is_playlist_loaded(playlist)
470 assert loaded == True, loaded
471 self._c['playlist'].update_playlist(playlist)
473 def _postprocess_get_curve(self, command, args={}, results=[]):
474 """Update `self` to show the curve.
476 if not isinstance(results[-1], Success):
477 self._postprocess_text(command, results=results)
479 assert len(results) == 2, results
481 if args.get('curve', None) == None:
482 # the command defaults to the current curve of the current playlist
483 results = self.execute_command(
484 command=self._command_by_name('get playlist'))
485 playlist = results[0]
487 raise NotImplementedError()
488 if 'note' in self._c:
489 self._c['note'].set_text(curve.info.get('note', ''))
490 if 'playlist' in self._c:
491 self._c['playlist'].set_selected_curve(
493 if 'plot' in self._c:
494 self._c['plot'].set_curve(curve, config=self.gui.config)
496 def _postprocess_next_curve(self, command, args={}, results=[]):
497 """No-op. Only call 'next curve' via `self._next_curve()`.
501 def _postprocess_previous_curve(self, command, args={}, results=[]):
502 """No-op. Only call 'previous curve' via `self._previous_curve()`.
506 def _postprocess_glob_curves_to_playlist(
507 self, command, args={}, results=[]):
508 """Update `self` to show new curves.
510 if not isinstance(results[-1], Success):
511 self._postprocess_text(command, results=results)
513 if 'playlist' in self._c:
514 if args.get('playlist', None) != None:
515 playlist = args['playlist']
516 pname = playlist.name
517 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
518 assert loaded == True, loaded
519 for curve in results[:-1]:
520 self._c['playlist']._add_curve(pname, curve)
522 self.execute_command(
523 command=self._command_by_name('get playlist'))
525 def _postprocess_zero_block_surface_contact_point(
526 self, command, args={}, results=[]):
527 """Update the curve, since the available columns may have changed.
529 if isinstance(results[-1], Success):
530 self.execute_command(
531 command=self._command_by_name('get curve'))
533 def _postprocess_add_block_force_array(
534 self, command, args={}, results=[]):
535 """Update the curve, since the available columns may have changed.
537 if isinstance(results[-1], Success):
538 self.execute_command(
539 command=self._command_by_name('get curve'))
543 # Command panel interface
545 def select_command(self, _class, method, command):
546 #self.select_plugin(plugin=command.plugin)
547 self._c['property editor'].clear()
548 self._c['property editor']._argument_from_label = {}
549 for argument in command.arguments:
550 if argument.name == 'help':
553 results = self.execute_command(
554 command=self._command_by_name('playlists'))
555 if not isinstance(results[-1], Success):
556 self._postprocess_text(command, results=results)
559 playlists = results[0]
561 results = self.execute_command(
562 command=self._command_by_name('playlist curves'))
563 if not isinstance(results[-1], Success):
564 self._postprocess_text(command, results=results)
569 ret = props_from_argument(
570 argument, curves=curves, playlists=playlists)
572 continue # property intentionally not handled (yet)
574 self._c['property editor'].append_property(p)
575 self._c['property editor']._argument_from_label[label] = (
578 self._set_config('selected command', command.name)
580 def select_plugin(self, _class=None, method=None, plugin=None):
585 # Folders panel interface
587 def _on_dir_ctrl_left_double_click(self, event):
588 file_path = self.panelFolders.GetPath()
589 if os.path.isfile(file_path):
590 if file_path.endswith('.hkp'):
591 self.do_loadlist(file_path)
596 # Note panel interface
598 def _on_update_note(self, _class, method, text):
599 """Sets the note for the active curve.
601 self.execute_command(
602 command=self._command_by_name('set note'),
607 # Playlist panel interface
609 def _on_user_delete_playlist(self, _class, method, playlist):
612 def _on_delete_playlist(self, _class, method, playlist):
613 if hasattr(playlist, 'path') and playlist.path != None:
614 os.remove(playlist.path)
616 def _on_user_delete_curve(self, _class, method, playlist, curve):
619 def _on_delete_curve(self, _class, method, playlist, curve):
620 # TODO: execute_command 'remove curve from playlist'
621 os.remove(curve.path)
623 def _on_set_selected_playlist(self, _class, method, playlist):
624 """Call the `jump to playlist` command.
626 results = self.execute_command(
627 command=self._command_by_name('playlists'))
628 if not isinstance(results[-1], Success):
630 assert len(results) == 2, results
631 playlists = results[0]
632 matching = [p for p in playlists if p.name == playlist.name]
633 assert len(matching) == 1, matching
634 index = playlists.index(matching[0])
635 results = self.execute_command(
636 command=self._command_by_name('jump to playlist'),
637 args={'index':index})
639 def _on_set_selected_curve(self, _class, method, playlist, curve):
640 """Call the `jump to curve` command.
642 self._on_set_selected_playlist(_class, method, playlist)
643 index = playlist.index(curve)
644 results = self.execute_command(
645 command=self._command_by_name('jump to curve'),
646 args={'index':index})
647 if not isinstance(results[-1], Success):
649 #results = self.execute_command(
650 # command=self._command_by_name('get playlist'))
651 #if not isinstance(results[-1], Success):
653 self.execute_command(
654 command=self._command_by_name('get curve'))
658 # Plot panel interface
660 def _on_plot_status_text(self, _class, method, text):
661 if 'status bar' in self._c:
662 self._c['status bar'].set_plot_text(text)
668 def _next_curve(self, *args):
669 """Call the `next curve` command.
671 results = self.execute_command(
672 command=self._command_by_name('next curve'))
673 if isinstance(results[-1], Success):
674 self.execute_command(
675 command=self._command_by_name('get curve'))
677 def _previous_curve(self, *args):
678 """Call the `previous curve` command.
680 results = self.execute_command(
681 command=self._command_by_name('previous curve'))
682 if isinstance(results[-1], Success):
683 self.execute_command(
684 command=self._command_by_name('get curve'))
688 # Panel display handling
690 def _on_pane_close(self, event):
692 view = self._c['menu bar']._c['view']
693 if pane.name in view._c.keys():
694 view._c[pane.name].Check(False)
697 def _on_panel_visibility(self, _class, method, panel_name, visible):
698 pane = self._c['manager'].GetPane(panel_name)
700 #if we don't do the following, the Folders pane does not resize properly on hide/show
701 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
702 #folders_size = pane.GetSize()
703 self.panelFolders.Fit()
704 self._c['manager'].Update()
706 def _setup_perspectives(self):
707 """Add perspectives to menubar and _perspectives.
709 self._perspectives = {
710 'Default': self._c['manager'].SavePerspective(),
712 path = os.path.expanduser(self.gui.config['perspective path'])
713 if os.path.isdir(path):
714 files = sorted(os.listdir(path))
716 name, extension = os.path.splitext(fname)
717 if extension != self.gui.config['perspective extension']:
719 fpath = os.path.join(path, fname)
720 if not os.path.isfile(fpath):
723 with open(fpath, 'rU') as f:
724 perspective = f.readline()
726 self._perspectives[name] = perspective
728 selected_perspective = self.gui.config['active perspective']
729 if not self._perspectives.has_key(selected_perspective):
730 self._set_config('active perspective', 'Default')
732 self._restore_perspective(selected_perspective, force=True)
733 self._update_perspective_menu()
735 def _update_perspective_menu(self):
736 self._c['menu bar']._c['perspective'].update(
737 sorted(self._perspectives.keys()),
738 self.gui.config['active perspective'])
740 def _save_perspective(self, perspective, perspective_dir, name,
742 path = os.path.join(perspective_dir, name)
743 if extension != None:
745 if not os.path.isdir(perspective_dir):
746 os.makedirs(perspective_dir)
747 with open(path, 'w') as f:
749 self._perspectives[name] = perspective
750 self._restore_perspective(name)
751 self._update_perspective_menu()
753 def _delete_perspectives(self, perspective_dir, names,
755 self.log.debug('remove perspectives %s from %s'
756 % (names, perspective_dir))
758 path = os.path.join(perspective_dir, name)
759 if extension != None:
762 del(self._perspectives[name])
763 self._update_perspective_menu()
764 if self.gui.config['active perspective'] in names:
765 self._restore_perspective('Default')
766 # TODO: does this bug still apply?
767 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
768 # http://trac.wxwidgets.org/ticket/3258
769 # ) that makes the radio item indicator in the menu disappear.
770 # The code should be fine once this issue is fixed.
772 def _restore_perspective(self, name, force=False):
773 if name != self.gui.config['active perspective'] or force == True:
774 self.log.debug('restore perspective %s' % name)
775 self._set_config('active perspective', name)
776 self._c['manager'].LoadPerspective(self._perspectives[name])
777 self._c['manager'].Update()
778 for pane in self._c['manager'].GetAllPanes():
779 view = self._c['menu bar']._c['view']
780 if pane.name in view._c.keys():
781 view._c[pane.name].Check(pane.window.IsShown())
783 def _on_save_perspective(self, *args):
784 perspective = self._c['manager'].SavePerspective()
785 name = self.gui.config['active perspective']
786 if name == 'Default':
787 name = 'New perspective'
788 name = select_save_file(
789 directory=os.path.expanduser(self.gui.config['perspective path']),
791 extension=self.gui.config['perspective extension'],
793 message='Enter a name for the new perspective:',
794 caption='Save perspective')
797 self._save_perspective(
799 os.path.expanduser(self.gui.config['perspective path']), name=name,
800 extension=self.gui.config['perspective extension'])
802 def _on_delete_perspective(self, *args, **kwargs):
803 options = sorted([p for p in self._perspectives.keys()
805 dialog = SelectionDialog(
807 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
808 button_id=wx.ID_DELETE,
809 selection_style='multiple',
811 title='Delete perspective(s)',
812 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
813 dialog.CenterOnScreen()
815 if dialog.canceled == True:
817 names = [options[i] for i in dialog.selected]
819 self._delete_perspectives(
820 os.path.expanduser(self.gui.config['perspective path']),
821 names=names, extension=self.gui.config['perspective extension'])
823 def _on_select_perspective(self, _class, method, name):
824 self._restore_perspective(name)
828 class HookeApp (wx.App):
829 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
831 Tosses up a splash screen and then loads :class:`HookeFrame` in
834 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
836 self.commands = commands
837 self.inqueue = inqueue
838 self.outqueue = outqueue
839 super(HookeApp, self).__init__(*args, **kwargs)
842 self.SetAppName('Hooke')
843 self.SetVendorName('')
844 self._setup_splash_screen()
846 height = self.gui.config['main height']
847 width = self.gui.config['main width']
848 top = self.gui.config['main top']
849 left = self.gui.config['main left']
851 # Sometimes, the ini file gets confused and sets 'left' and
852 # 'top' to large negative numbers. Here we catch and fix
853 # this. Keep small negative numbers, the user might want
862 self.gui, self.commands, self.inqueue, self.outqueue,
863 parent=None, title='Hooke',
864 pos=(left, top), size=(width, height),
865 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
867 self._c['frame'].Show(True)
868 self.SetTopWindow(self._c['frame'])
871 def _setup_splash_screen(self):
872 if self.gui.config['show splash screen'] == True:
873 path = os.path.expanduser(self.gui.config['splash screen image'])
874 if os.path.isfile(path):
875 duration = self.gui.config['splash screen duration']
877 bitmap=wx.Image(path).ConvertToBitmap(),
878 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
879 milliseconds=duration,
882 # For some reason splashDuration and sleep do not
883 # correspond to each other at least not on Windows.
884 # Maybe it's because duration is in milliseconds and
885 # sleep in seconds. Thus we need to increase the
886 # sleep time a bit. A factor of 1.2 seems to work.
888 time.sleep(sleepFactor * duration / 1000)
891 class GUI (UserInterface):
892 """wxWindows graphical user interface.
895 super(GUI, self).__init__(name='gui')
897 def default_settings(self):
898 """Return a list of :class:`hooke.config.Setting`\s for any
899 configurable UI settings.
901 The suggested section setting is::
903 Setting(section=self.setting_section, help=self.__doc__)
906 Setting(section=self.setting_section, help=self.__doc__),
907 Setting(section=self.setting_section, option='icon image',
908 value=os.path.join('doc', 'img', 'microscope.ico'),
910 help='Path to the hooke icon image.'),
911 Setting(section=self.setting_section, option='show splash screen',
912 value=True, type='bool',
913 help='Enable/disable the splash screen'),
914 Setting(section=self.setting_section, option='splash screen image',
915 value=os.path.join('doc', 'img', 'hooke.jpg'),
917 help='Path to the Hooke splash screen image.'),
918 Setting(section=self.setting_section,
919 option='splash screen duration',
920 value=1000, type='int',
921 help='Duration of the splash screen in milliseconds.'),
922 Setting(section=self.setting_section, option='perspective path',
923 value=os.path.join('resources', 'gui', 'perspective'),
924 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
925 Setting(section=self.setting_section, option='perspective extension',
927 help='Extension for perspective files.'),
928 Setting(section=self.setting_section, option='hide extensions',
929 value=False, type='bool',
930 help='Hide file extensions when displaying names.'),
931 Setting(section=self.setting_section, option='plot legend',
932 value=True, type='bool',
933 help='Enable/disable the plot legend.'),
934 Setting(section=self.setting_section, option='plot SI format',
935 value='True', type='bool',
936 help='Enable/disable SI plot axes numbering.'),
937 Setting(section=self.setting_section, option='plot decimals',
939 help='Number of decimal places to show if "plot SI format" is enabled.'),
940 Setting(section=self.setting_section, option='folders-workdir',
941 value='.', type='path',
942 help='This should probably go...'),
943 Setting(section=self.setting_section, option='folders-filters',
944 value='.', type='path',
945 help='This should probably go...'),
946 Setting(section=self.setting_section, option='active perspective',
948 help='Name of active perspective file (or "Default").'),
949 Setting(section=self.setting_section,
950 option='folders-filter-index',
952 help='This should probably go...'),
953 Setting(section=self.setting_section, option='main height',
954 value=450, type='int',
955 help='Height of main window in pixels.'),
956 Setting(section=self.setting_section, option='main width',
957 value=800, type='int',
958 help='Width of main window in pixels.'),
959 Setting(section=self.setting_section, option='main top',
961 help='Pixels from screen top to top of main window.'),
962 Setting(section=self.setting_section, option='main left',
964 help='Pixels from screen left to left of main window.'),
965 Setting(section=self.setting_section, option='selected command',
966 value='load playlist',
967 help='Name of the initially selected command.'),
970 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
974 app = HookeApp(gui=self,
976 inqueue=ui_to_command_queue,
977 outqueue=command_to_ui_queue,
981 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
982 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)