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}
74 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
77 self._c['manager'] = aui.AuiManager()
78 self._c['manager'].SetManagedWindow(self)
80 # set the gradient and drag styles
81 self._c['manager'].GetArtProvider().SetMetric(
82 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
83 self._c['manager'].SetFlags(
84 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
86 # Min size for the frame itself isn't completely done. See
87 # the end of FrameManager::Update() for the test code. For
88 # now, just hard code a frame minimum size.
89 #self.SetMinSize(wx.Size(500, 500))
92 self._setup_toolbars()
93 self._c['manager'].Update() # commit pending changes
95 # Create the menubar after the panes so that the default
96 # perspective is created with all panes open
97 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
98 self._c['menu bar'] = menu.HookeMenuBar(
102 'close': self._on_close,
103 'about': self._on_about,
104 'view_panel': self._on_panel_visibility,
105 'save_perspective': self._on_save_perspective,
106 'delete_perspective': self._on_delete_perspective,
107 'select_perspective': self._on_select_perspective,
109 self.SetMenuBar(self._c['menu bar'])
111 self._c['status bar'] = statusbar.StatusBar(
113 style=wx.ST_SIZEGRIP)
114 self.SetStatusBar(self._c['status bar'])
116 self._setup_perspectives()
118 return # TODO: cleanup
119 self._displayed_plot = None
120 #load default list, if possible
121 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
126 def _setup_panels(self):
127 client_size = self.GetClientSize()
129 # ('folders', wx.GenericDirCtrl(
131 # dir=self.gui.config['folders-workdir'],
133 # style=wx.DIRCTRL_SHOW_FILTERS,
134 # filter=self.gui.config['folders-filters'],
135 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
136 (panel.PANELS['playlist'](
138 'delete_playlist':self._on_user_delete_playlist,
139 '_delete_playlist':self._on_delete_playlist,
140 'delete_curve':self._on_user_delete_curve,
141 '_delete_curve':self._on_delete_curve,
142 '_on_set_selected_playlist':self._on_set_selected_playlist,
143 '_on_set_selected_curve':self._on_set_selected_curve,
146 style=wx.WANTS_CHARS|wx.NO_BORDER,
147 # WANTS_CHARS so the panel doesn't eat the Return key.
150 (panel.PANELS['note'](
152 '_on_update':self._on_update_note,
155 style=wx.WANTS_CHARS|wx.NO_BORDER,
158 # ('notebook', Notebook(
160 # pos=wx.Point(client_size.x, client_size.y),
161 # size=wx.Size(430, 200),
162 # style=aui.AUI_NB_DEFAULT_STYLE
163 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
164 (panel.PANELS['commands'](
165 commands=self.commands,
166 selected=self.gui.config['selected command'],
168 'execute': self.execute_command,
169 'select_plugin': self.select_plugin,
170 'select_command': self.select_command,
171 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
174 style=wx.WANTS_CHARS|wx.NO_BORDER,
175 # WANTS_CHARS so the panel doesn't eat the Return key.
178 (panel.PANELS['propertyeditor'](
181 style=wx.WANTS_CHARS,
182 # WANTS_CHARS so the panel doesn't eat the Return key.
184 (panel.PANELS['plot'](
186 '_set_status_text': self._on_plot_status_text,
189 style=wx.WANTS_CHARS|wx.NO_BORDER,
190 # WANTS_CHARS so the panel doesn't eat the Return key.
193 (panel.PANELS['output'](
196 size=wx.Size(150, 90),
197 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
199 # ('results', panel.results.Results(self), 'bottom'),
201 self._add_panel(p, style)
202 self.execute_command( # setup already loaded playlists
203 command=self._command_by_name('playlists'))
204 self.execute_command( # setup already loaded curve
205 command=self._command_by_name('get curve'))
207 def _add_panel(self, panel, style):
208 self._c[panel.name] = panel
209 m_name = panel.managed_name
210 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
211 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
214 elif style == 'center':
216 elif style == 'left':
218 elif style == 'right':
221 assert style == 'bottom', style
223 self._c['manager'].AddPane(panel, info)
225 def _setup_toolbars(self):
226 self._c['navigation bar'] = navbar.NavBar(
228 'next': self._next_curve,
229 'previous': self._previous_curve,
232 style=wx.TB_FLAT | wx.TB_NODIVIDER)
233 self._c['manager'].AddPane(
234 self._c['navigation bar'],
235 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
236 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
237 ).RightDockable(False))
239 def _bind_events(self):
240 # TODO: figure out if we can use the eventManager for menu
241 # ranges and events of 'self' without raising an assertion
243 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
244 self.Bind(wx.EVT_SIZE, self._on_size)
245 self.Bind(wx.EVT_CLOSE, self._on_close)
246 self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
247 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_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 execute_command(self, _class=None, method=None,
305 command=None, args=None):
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(cm, self.inqueue)
342 return self._handle_response(command_message=cm)
344 def _handle_response(self, command_message):
347 msg = self.outqueue.get()
349 if isinstance(msg, Exit):
352 elif isinstance(msg, CommandExit):
353 # TODO: display command complete
355 elif isinstance(msg, ReloadUserInterfaceConfig):
356 self.gui.reload_config(msg.config)
358 elif isinstance(msg, Request):
359 h = handler.HANDLERS[msg.type]
360 h.run(self, msg) # TODO: pause for response?
363 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
364 self._postprocess_text)
365 pp(command=command_message.command,
366 args=command_message.arguments,
370 def _handle_request(self, msg):
371 """Repeatedly try to get a response to `msg`.
374 raise NotImplementedError('_%s_request_prompt' % msg.type)
375 prompt_string = prompt(msg)
376 parser = getattr(self, '_%s_request_parser' % msg.type, None)
378 raise NotImplementedError('_%s_request_parser' % msg.type)
382 self.cmd.stdout.write(''.join([
383 error.__class__.__name__, ': ', str(error), '\n']))
384 self.cmd.stdout.write(prompt_string)
385 value = parser(msg, self.cmd.stdin.readline())
387 response = msg.response(value)
389 except ValueError, error:
391 self.inqueue.put(response)
393 def _set_config(self, option, value, section=None):
394 self.gui._set_config(section=section, option=option, value=value,
395 ui_to_command_queue=self.inqueue,
396 response_handler=self._handle_response)
399 # Command-specific postprocessing
401 def _postprocess_text(self, command, args={}, results=[]):
402 """Print the string representation of the results to the Results window.
404 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
405 approach, except that :class:`~hooke.ui.commandline.DoCommand`
406 doesn't print some internally handled messages
407 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
409 for result in results:
410 if isinstance(result, CommandExit):
411 self._c['output'].write(result.__class__.__name__+'\n')
412 self._c['output'].write(str(result).rstrip()+'\n')
414 def _postprocess_playlists(self, command, args={}, results=None):
415 """Update `self` to show the playlists.
417 if not isinstance(results[-1], Success):
418 self._postprocess_text(command, results=results)
420 assert len(results) == 2, results
421 playlists = results[0]
422 if 'playlist' in self._c:
423 for playlist in playlists:
424 if self._c['playlist'].is_playlist_loaded(playlist):
425 self._c['playlist'].update_playlist(playlist)
427 self._c['playlist'].add_playlist(playlist)
429 def _postprocess_new_playlist(self, command, args={}, results=None):
430 """Update `self` to show the new playlist.
432 if not isinstance(results[-1], Success):
433 self._postprocess_text(command, results=results)
435 assert len(results) == 2, results
436 playlist = results[0]
437 if 'playlist' in self._c:
438 loaded = self._c['playlist'].is_playlist_loaded(playlist)
439 assert loaded == False, loaded
440 self._c['playlist'].add_playlist(playlist)
442 def _postprocess_load_playlist(self, command, args={}, results=None):
443 """Update `self` to show the playlist.
445 if not isinstance(results[-1], Success):
446 self._postprocess_text(command, results=results)
448 assert len(results) == 2, results
449 playlist = results[0]
450 self._c['playlist'].add_playlist(playlist)
452 def _postprocess_get_playlist(self, command, args={}, results=[]):
453 if not isinstance(results[-1], Success):
454 self._postprocess_text(command, results=results)
456 assert len(results) == 2, results
457 playlist = results[0]
458 if 'playlist' in self._c:
459 loaded = self._c['playlist'].is_playlist_loaded(playlist)
460 assert loaded == True, loaded
461 self._c['playlist'].update_playlist(playlist)
463 def _postprocess_get_curve(self, command, args={}, results=[]):
464 """Update `self` to show the curve.
466 if not isinstance(results[-1], Success):
467 self._postprocess_text(command, results=results)
469 assert len(results) == 2, results
471 if args.get('curve', None) == None:
472 # the command defaults to the current curve of the current playlist
473 results = self.execute_command(
474 command=self._command_by_name('get playlist'))
475 playlist = results[0]
477 raise NotImplementedError()
478 if 'note' in self._c:
479 self._c['note'].set_text(curve.info.get('note', ''))
480 if 'playlist' in self._c:
481 self._c['playlist'].set_selected_curve(
483 if 'plot' in self._c:
484 self._c['plot'].set_curve(curve, config=self.gui.config)
486 def _postprocess_next_curve(self, command, args={}, results=[]):
487 """No-op. Only call 'next curve' via `self._next_curve()`.
491 def _postprocess_previous_curve(self, command, args={}, results=[]):
492 """No-op. Only call 'previous curve' via `self._previous_curve()`.
496 def _postprocess_glob_curves_to_playlist(
497 self, command, args={}, results=[]):
498 """Update `self` to show new curves.
500 if not isinstance(results[-1], Success):
501 self._postprocess_text(command, results=results)
503 if 'playlist' in self._c:
504 if args.get('playlist', None) != None:
505 playlist = args['playlist']
506 pname = playlist.name
507 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
508 assert loaded == True, loaded
509 for curve in results[:-1]:
510 self._c['playlist']._add_curve(pname, curve)
512 self.execute_command(
513 command=self._command_by_name('get playlist'))
515 def _postprocess_zero_block_surface_contact_point(
516 self, command, args={}, results=[]):
517 """Update the curve, since the available columns may have changed.
519 if isinstance(results[-1], Success):
520 self.execute_command(
521 command=self._command_by_name('get curve'))
523 def _postprocess_add_block_force_array(
524 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'))
533 # Command panel interface
535 def select_command(self, _class, method, command):
536 #self.select_plugin(plugin=command.plugin)
537 self._c['property editor'].clear()
538 self._c['property editor']._argument_from_label = {}
539 for argument in command.arguments:
540 if argument.name == 'help':
543 results = self.execute_command(
544 command=self._command_by_name('playlists'))
545 if not isinstance(results[-1], Success):
546 self._postprocess_text(command, results=results)
549 playlists = results[0]
551 results = self.execute_command(
552 command=self._command_by_name('playlist curves'))
553 if not isinstance(results[-1], Success):
554 self._postprocess_text(command, results=results)
559 ret = props_from_argument(
560 argument, curves=curves, playlists=playlists)
562 continue # property intentionally not handled (yet)
564 self._c['property editor'].append_property(p)
565 self._c['property editor']._argument_from_label[label] = (
568 self._set_config('selected command', command.name)
570 def select_plugin(self, _class=None, method=None, plugin=None):
575 # Folders panel interface
577 def _on_dir_ctrl_left_double_click(self, event):
578 file_path = self.panelFolders.GetPath()
579 if os.path.isfile(file_path):
580 if file_path.endswith('.hkp'):
581 self.do_loadlist(file_path)
586 # Note panel interface
588 def _on_update_note(self, _class, method, text):
589 """Sets the note for the active curve.
591 self.execute_command(
592 command=self._command_by_name('set note'),
597 # Playlist panel interface
599 def _on_user_delete_playlist(self, _class, method, playlist):
602 def _on_delete_playlist(self, _class, method, playlist):
603 if hasattr(playlist, 'path') and playlist.path != None:
604 os.remove(playlist.path)
606 def _on_user_delete_curve(self, _class, method, playlist, curve):
609 def _on_delete_curve(self, _class, method, playlist, curve):
610 # TODO: execute_command 'remove curve from playlist'
611 os.remove(curve.path)
613 def _on_set_selected_playlist(self, _class, method, playlist):
614 """Call the `jump to playlist` command.
616 results = self.execute_command(
617 command=self._command_by_name('playlists'))
618 if not isinstance(results[-1], Success):
620 assert len(results) == 2, results
621 playlists = results[0]
622 matching = [p for p in playlists if p.name == playlist.name]
623 assert len(matching) == 1, matching
624 index = playlists.index(matching[0])
625 results = self.execute_command(
626 command=self._command_by_name('jump to playlist'),
627 args={'index':index})
629 def _on_set_selected_curve(self, _class, method, playlist, curve):
630 """Call the `jump to curve` command.
632 self._on_set_selected_playlist(_class, method, playlist)
633 index = playlist.index(curve)
634 results = self.execute_command(
635 command=self._command_by_name('jump to curve'),
636 args={'index':index})
637 if not isinstance(results[-1], Success):
639 #results = self.execute_command(
640 # command=self._command_by_name('get playlist'))
641 #if not isinstance(results[-1], Success):
643 self.execute_command(
644 command=self._command_by_name('get curve'))
648 # Plot panel interface
650 def _on_plot_status_text(self, _class, method, text):
651 if 'status bar' in self._c:
652 self._c['status bar'].set_plot_text(text)
658 def _next_curve(self, *args):
659 """Call the `next curve` command.
661 results = self.execute_command(
662 command=self._command_by_name('next curve'))
663 if isinstance(results[-1], Success):
664 self.execute_command(
665 command=self._command_by_name('get curve'))
667 def _previous_curve(self, *args):
668 """Call the `previous curve` command.
670 results = self.execute_command(
671 command=self._command_by_name('previous curve'))
672 if isinstance(results[-1], Success):
673 self.execute_command(
674 command=self._command_by_name('get curve'))
678 # Panel display handling
680 def _on_pane_close(self, event):
682 view = self._c['menu bar']._c['view']
683 if pane.name in view._c.keys():
684 view._c[pane.name].Check(False)
687 def _on_panel_visibility(self, _class, method, panel_name, visible):
688 pane = self._c['manager'].GetPane(panel_name)
690 #if we don't do the following, the Folders pane does not resize properly on hide/show
691 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
692 #folders_size = pane.GetSize()
693 self.panelFolders.Fit()
694 self._c['manager'].Update()
696 def _setup_perspectives(self):
697 """Add perspectives to menubar and _perspectives.
699 self._perspectives = {
700 'Default': self._c['manager'].SavePerspective(),
702 path = self.gui.config['perspective path']
703 if os.path.isdir(path):
704 files = sorted(os.listdir(path))
706 name, extension = os.path.splitext(fname)
707 if extension != self.gui.config['perspective extension']:
709 fpath = os.path.join(path, fname)
710 if not os.path.isfile(fpath):
713 with open(fpath, 'rU') as f:
714 perspective = f.readline()
716 self._perspectives[name] = perspective
718 selected_perspective = self.gui.config['active perspective']
719 if not self._perspectives.has_key(selected_perspective):
720 self._set_config('active perspective', 'Default')
722 self._restore_perspective(selected_perspective, force=True)
723 self._update_perspective_menu()
725 def _update_perspective_menu(self):
726 self._c['menu bar']._c['perspective'].update(
727 sorted(self._perspectives.keys()),
728 self.gui.config['active perspective'])
730 def _save_perspective(self, perspective, perspective_dir, name,
732 path = os.path.join(perspective_dir, name)
733 if extension != None:
735 if not os.path.isdir(perspective_dir):
736 os.makedirs(perspective_dir)
737 with open(path, 'w') as f:
739 self._perspectives[name] = perspective
740 self._restore_perspective(name)
741 self._update_perspective_menu()
743 def _delete_perspectives(self, perspective_dir, names,
745 self.log.debug('remove perspectives %s from %s'
746 % (names, perspective_dir))
748 path = os.path.join(perspective_dir, name)
749 if extension != None:
752 del(self._perspectives[name])
753 self._update_perspective_menu()
754 if self.gui.config['active perspective'] in names:
755 self._restore_perspective('Default')
756 # TODO: does this bug still apply?
757 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
758 # http://trac.wxwidgets.org/ticket/3258
759 # ) that makes the radio item indicator in the menu disappear.
760 # The code should be fine once this issue is fixed.
762 def _restore_perspective(self, name, force=False):
763 if name != self.gui.config['active perspective'] or force == True:
764 self.log.debug('restore perspective %s' % name)
765 self._set_config('active perspective', name)
766 self._c['manager'].LoadPerspective(self._perspectives[name])
767 self._c['manager'].Update()
768 for pane in self._c['manager'].GetAllPanes():
769 view = self._c['menu bar']._c['view']
770 if pane.name in view._c.keys():
771 view._c[pane.name].Check(pane.window.IsShown())
773 def _on_save_perspective(self, *args):
774 perspective = self._c['manager'].SavePerspective()
775 name = self.gui.config['active perspective']
776 if name == 'Default':
777 name = 'New perspective'
778 name = select_save_file(
779 directory=self.gui.config['perspective path'],
781 extension=self.gui.config['perspective extension'],
783 message='Enter a name for the new perspective:',
784 caption='Save perspective')
787 self._save_perspective(
788 perspective, self.gui.config['perspective path'], name=name,
789 extension=self.gui.config['perspective extension'])
791 def _on_delete_perspective(self, *args, **kwargs):
792 options = sorted([p for p in self._perspectives.keys()
794 dialog = SelectionDialog(
796 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
797 button_id=wx.ID_DELETE,
798 selection_style='multiple',
800 title='Delete perspective(s)',
801 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
802 dialog.CenterOnScreen()
804 if dialog.canceled == True:
806 names = [options[i] for i in dialog.selected]
808 self._delete_perspectives(
809 self.gui.config['perspective path'], names=names,
810 extension=self.gui.config['perspective extension'])
812 def _on_select_perspective(self, _class, method, name):
813 self._restore_perspective(name)
817 class HookeApp (wx.App):
818 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
820 Tosses up a splash screen and then loads :class:`HookeFrame` in
823 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
825 self.commands = commands
826 self.inqueue = inqueue
827 self.outqueue = outqueue
828 super(HookeApp, self).__init__(*args, **kwargs)
831 self.SetAppName('Hooke')
832 self.SetVendorName('')
833 self._setup_splash_screen()
835 height = self.gui.config['main height']
836 width = self.gui.config['main width']
837 top = self.gui.config['main top']
838 left = self.gui.config['main left']
840 # Sometimes, the ini file gets confused and sets 'left' and
841 # 'top' to large negative numbers. Here we catch and fix
842 # this. Keep small negative numbers, the user might want
851 self.gui, self.commands, self.inqueue, self.outqueue,
852 parent=None, title='Hooke',
853 pos=(left, top), size=(width, height),
854 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
856 self._c['frame'].Show(True)
857 self.SetTopWindow(self._c['frame'])
860 def _setup_splash_screen(self):
861 if self.gui.config['show splash screen'] == True:
862 path = self.gui.config['splash screen image']
863 if os.path.isfile(path):
864 duration = self.gui.config['splash screen duration']
866 bitmap=wx.Image(path).ConvertToBitmap(),
867 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
868 milliseconds=duration,
871 # For some reason splashDuration and sleep do not
872 # correspond to each other at least not on Windows.
873 # Maybe it's because duration is in milliseconds and
874 # sleep in seconds. Thus we need to increase the
875 # sleep time a bit. A factor of 1.2 seems to work.
877 time.sleep(sleepFactor * duration / 1000)
880 class GUI (UserInterface):
881 """wxWindows graphical user interface.
884 super(GUI, self).__init__(name='gui')
886 def default_settings(self):
887 """Return a list of :class:`hooke.config.Setting`\s for any
888 configurable UI settings.
890 The suggested section setting is::
892 Setting(section=self.setting_section, help=self.__doc__)
895 Setting(section=self.setting_section, help=self.__doc__),
896 Setting(section=self.setting_section, option='icon image',
897 value=os.path.join('doc', 'img', 'microscope.ico'),
899 help='Path to the hooke icon image.'),
900 Setting(section=self.setting_section, option='show splash screen',
901 value=True, type='bool',
902 help='Enable/disable the splash screen'),
903 Setting(section=self.setting_section, option='splash screen image',
904 value=os.path.join('doc', 'img', 'hooke.jpg'),
906 help='Path to the Hooke splash screen image.'),
907 Setting(section=self.setting_section,
908 option='splash screen duration',
909 value=1000, type='int',
910 help='Duration of the splash screen in milliseconds.'),
911 Setting(section=self.setting_section, option='perspective path',
912 value=os.path.join('resources', 'gui', 'perspective'),
913 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
914 Setting(section=self.setting_section, option='perspective extension',
916 help='Extension for perspective files.'),
917 Setting(section=self.setting_section, option='hide extensions',
918 value=False, type='bool',
919 help='Hide file extensions when displaying names.'),
920 Setting(section=self.setting_section, option='plot legend',
921 value=True, type='bool',
922 help='Enable/disable the plot legend.'),
923 Setting(section=self.setting_section, option='plot SI format',
924 value='True', type='bool',
925 help='Enable/disable SI plot axes numbering.'),
926 Setting(section=self.setting_section, option='plot decimals',
928 help='Number of decimal places to show if "plot SI format" is enabled.'),
929 Setting(section=self.setting_section, option='folders-workdir',
930 value='.', type='path',
931 help='This should probably go...'),
932 Setting(section=self.setting_section, option='folders-filters',
933 value='.', type='path',
934 help='This should probably go...'),
935 Setting(section=self.setting_section, option='active perspective',
937 help='Name of active perspective file (or "Default").'),
938 Setting(section=self.setting_section,
939 option='folders-filter-index',
941 help='This should probably go...'),
942 Setting(section=self.setting_section, option='main height',
943 value=450, type='int',
944 help='Height of main window in pixels.'),
945 Setting(section=self.setting_section, option='main width',
946 value=800, type='int',
947 help='Width of main window in pixels.'),
948 Setting(section=self.setting_section, option='main top',
950 help='Pixels from screen top to top of main window.'),
951 Setting(section=self.setting_section, option='main left',
953 help='Pixels from screen left to left of main window.'),
954 Setting(section=self.setting_section, option='selected command',
955 value='load playlist',
956 help='Name of the initially selected command.'),
959 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
963 app = HookeApp(gui=self,
965 inqueue=ui_to_command_queue,
966 outqueue=command_to_ui_queue,
970 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
971 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)