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)
248 return # TODO: cleanup
249 treeCtrl = self._c['folders'].GetTreeCtrl()
250 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
252 def _on_about(self, *args):
253 dialog = wx.MessageDialog(
255 message=self.gui._splash_text(extra_info={
256 'get-details':'click "Help -> License"'},
258 caption='About Hooke',
259 style=wx.OK|wx.ICON_INFORMATION)
263 def _on_size(self, event):
266 def _on_close(self, *args):
267 self.log.info('closing GUI framework')
269 self._set_config('main height', self.GetSize().GetHeight())
270 self._set_config('main left', self.GetPosition()[0])
271 self._set_config('main top', self.GetPosition()[1])
272 self._set_config('main width', self.GetSize().GetWidth())
273 self._c['manager'].UnInit()
274 del self._c['manager']
277 def _on_erase_background(self, event):
282 # Panel utility functions
284 def _file_name(self, name):
285 """Cleanup names according to configured preferences.
287 if self.gui.config['hide extensions'] == True:
288 name,ext = os.path.splitext(name)
295 def _command_by_name(self, name):
296 cs = [c for c in self.commands if c.name == name]
300 raise Exception('Multiple commands named "%s"' % name)
303 def execute_command(self, _class=None, method=None,
304 command=None, args=None):
307 if ('property editor' in self._c
308 and self.gui.config['selected command'] == command.name):
309 for name,value in self._c['property editor'].get_values().items():
310 arg = self._c['property editor']._argument_from_label.get(
315 args[arg.name] = value
317 # deal with counted arguments
318 if arg.name not in args:
320 index = int(name[len(arg.name):])
321 args[arg.name][index] = value
322 for arg in command.arguments:
323 if arg.name not in args:
324 continue # undisplayed argument, e.g. 'driver' types.
326 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
327 count = arg._display_count
328 if count != 1 and arg.name in args:
329 keys = sorted(args[arg.name].keys())
330 assert keys == range(count), keys
331 args[arg.name] = [args[arg.name][i]
332 for i in range(count)]
334 while (len(args[arg.name]) > 0
335 and args[arg.name][-1] == None):
337 if len(args[arg.name]) == 0:
338 args[arg.name] = arg.default
339 cm = CommandMessage(command.name, args)
340 self.gui._submit_command(cm, self.inqueue)
341 return self._handle_response(command_message=cm)
343 def _handle_response(self, command_message):
346 msg = self.outqueue.get()
348 if isinstance(msg, Exit):
351 elif isinstance(msg, CommandExit):
352 # TODO: display command complete
354 elif isinstance(msg, ReloadUserInterfaceConfig):
355 self.gui.reload_config(msg.config)
357 elif isinstance(msg, Request):
358 h = handler.HANDLERS[msg.type]
359 h.run(self, msg) # TODO: pause for response?
362 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
363 self._postprocess_text)
364 pp(command=command_message.command,
365 args=command_message.arguments,
369 def _handle_request(self, msg):
370 """Repeatedly try to get a response to `msg`.
373 raise NotImplementedError('_%s_request_prompt' % msg.type)
374 prompt_string = prompt(msg)
375 parser = getattr(self, '_%s_request_parser' % msg.type, None)
377 raise NotImplementedError('_%s_request_parser' % msg.type)
381 self.cmd.stdout.write(''.join([
382 error.__class__.__name__, ': ', str(error), '\n']))
383 self.cmd.stdout.write(prompt_string)
384 value = parser(msg, self.cmd.stdin.readline())
386 response = msg.response(value)
388 except ValueError, error:
390 self.inqueue.put(response)
392 def _set_config(self, option, value, section=None):
393 self.gui._set_config(section=section, option=option, value=value,
394 ui_to_command_queue=self.inqueue,
395 response_handler=self._handle_response)
398 # Command-specific postprocessing
400 def _postprocess_text(self, command, args={}, results=[]):
401 """Print the string representation of the results to the Results window.
403 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
404 approach, except that :class:`~hooke.ui.commandline.DoCommand`
405 doesn't print some internally handled messages
406 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
408 for result in results:
409 if isinstance(result, CommandExit):
410 self._c['output'].write(result.__class__.__name__+'\n')
411 self._c['output'].write(str(result).rstrip()+'\n')
413 def _postprocess_playlists(self, command, args={}, results=None):
414 """Update `self` to show the playlists.
416 if not isinstance(results[-1], Success):
417 self._postprocess_text(command, results=results)
419 assert len(results) == 2, results
420 playlists = results[0]
421 if 'playlist' in self._c:
422 for playlist in playlists:
423 if self._c['playlist'].is_playlist_loaded(playlist):
424 self._c['playlist'].update_playlist(playlist)
426 self._c['playlist'].add_playlist(playlist)
428 def _postprocess_new_playlist(self, command, args={}, results=None):
429 """Update `self` to show the new playlist.
431 if not isinstance(results[-1], Success):
432 self._postprocess_text(command, results=results)
434 assert len(results) == 2, results
435 playlist = results[0]
436 if 'playlist' in self._c:
437 loaded = self._c['playlist'].is_playlist_loaded(playlist)
438 assert loaded == False, loaded
439 self._c['playlist'].add_playlist(playlist)
441 def _postprocess_load_playlist(self, command, args={}, results=None):
442 """Update `self` to show the playlist.
444 if not isinstance(results[-1], Success):
445 self._postprocess_text(command, results=results)
447 assert len(results) == 2, results
448 playlist = results[0]
449 self._c['playlist'].add_playlist(playlist)
451 def _postprocess_get_playlist(self, command, args={}, results=[]):
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 if 'playlist' in self._c:
458 loaded = self._c['playlist'].is_playlist_loaded(playlist)
459 assert loaded == True, loaded
460 self._c['playlist'].update_playlist(playlist)
462 def _postprocess_get_curve(self, command, args={}, results=[]):
463 """Update `self` to show the curve.
465 if not isinstance(results[-1], Success):
466 self._postprocess_text(command, results=results)
468 assert len(results) == 2, results
470 if args.get('curve', None) == None:
471 # the command defaults to the current curve of the current playlist
472 results = self.execute_command(
473 command=self._command_by_name('get playlist'))
474 playlist = results[0]
476 raise NotImplementedError()
477 if 'note' in self._c:
478 self._c['note'].set_text(curve.info.get('note', ''))
479 if 'playlist' in self._c:
480 self._c['playlist'].set_selected_curve(
482 if 'plot' in self._c:
483 self._c['plot'].set_curve(curve, config=self.gui.config)
485 def _postprocess_next_curve(self, command, args={}, results=[]):
486 """No-op. Only call 'next curve' via `self._next_curve()`.
490 def _postprocess_previous_curve(self, command, args={}, results=[]):
491 """No-op. Only call 'previous curve' via `self._previous_curve()`.
495 def _postprocess_glob_curves_to_playlist(
496 self, command, args={}, results=[]):
497 """Update `self` to show new curves.
499 if not isinstance(results[-1], Success):
500 self._postprocess_text(command, results=results)
502 if 'playlist' in self._c:
503 if args.get('playlist', None) != None:
504 playlist = args['playlist']
505 pname = playlist.name
506 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
507 assert loaded == True, loaded
508 for curve in results[:-1]:
509 self._c['playlist']._add_curve(pname, curve)
511 self.execute_command(
512 command=self._command_by_name('get playlist'))
514 def _postprocess_zero_block_surface_contact_point(
515 self, command, args={}, results=[]):
516 """Update the curve, since the available columns may have changed.
518 if isinstance(results[-1], Success):
519 self.execute_command(
520 command=self._command_by_name('get curve'))
522 def _postprocess_add_block_force_array(
523 self, command, args={}, results=[]):
524 """Update the curve, since the available columns may have changed.
526 if isinstance(results[-1], Success):
527 self.execute_command(
528 command=self._command_by_name('get curve'))
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 # TODO: execute_command 'remove curve from playlist'
610 os.remove(curve.path)
612 def _on_set_selected_playlist(self, _class, method, playlist):
613 """Call the `jump to playlist` command.
615 results = self.execute_command(
616 command=self._command_by_name('playlists'))
617 if not isinstance(results[-1], Success):
619 assert len(results) == 2, results
620 playlists = results[0]
621 matching = [p for p in playlists if p.name == playlist.name]
622 assert len(matching) == 1, matching
623 index = playlists.index(matching[0])
624 results = self.execute_command(
625 command=self._command_by_name('jump to playlist'),
626 args={'index':index})
628 def _on_set_selected_curve(self, _class, method, playlist, curve):
629 """Call the `jump to curve` command.
631 self._on_set_selected_playlist(_class, method, playlist)
632 index = playlist.index(curve)
633 results = self.execute_command(
634 command=self._command_by_name('jump to curve'),
635 args={'index':index})
636 if not isinstance(results[-1], Success):
638 #results = self.execute_command(
639 # command=self._command_by_name('get playlist'))
640 #if not isinstance(results[-1], Success):
642 self.execute_command(
643 command=self._command_by_name('get curve'))
647 # Plot panel interface
649 def _on_plot_status_text(self, _class, method, text):
650 if 'status bar' in self._c:
651 self._c['status bar'].set_plot_text(text)
657 def _next_curve(self, *args):
658 """Call the `next curve` command.
660 results = self.execute_command(
661 command=self._command_by_name('next curve'))
662 if isinstance(results[-1], Success):
663 self.execute_command(
664 command=self._command_by_name('get curve'))
666 def _previous_curve(self, *args):
667 """Call the `previous curve` command.
669 results = self.execute_command(
670 command=self._command_by_name('previous curve'))
671 if isinstance(results[-1], Success):
672 self.execute_command(
673 command=self._command_by_name('get curve'))
677 # Panel display handling
679 def _on_pane_close(self, event):
681 view = self._c['menu bar']._c['view']
682 if pane.name in view._c.keys():
683 view._c[pane.name].Check(False)
686 def _on_panel_visibility(self, _class, method, panel_name, visible):
687 pane = self._c['manager'].GetPane(panel_name)
689 #if we don't do the following, the Folders pane does not resize properly on hide/show
690 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
691 #folders_size = pane.GetSize()
692 self.panelFolders.Fit()
693 self._c['manager'].Update()
695 def _setup_perspectives(self):
696 """Add perspectives to menubar and _perspectives.
698 self._perspectives = {
699 'Default': self._c['manager'].SavePerspective(),
701 path = self.gui.config['perspective path']
702 if os.path.isdir(path):
703 files = sorted(os.listdir(path))
705 name, extension = os.path.splitext(fname)
706 if extension != self.gui.config['perspective extension']:
708 fpath = os.path.join(path, fname)
709 if not os.path.isfile(fpath):
712 with open(fpath, 'rU') as f:
713 perspective = f.readline()
715 self._perspectives[name] = perspective
717 selected_perspective = self.gui.config['active perspective']
718 if not self._perspectives.has_key(selected_perspective):
719 self._set_config('active perspective', 'Default')
721 self._restore_perspective(selected_perspective, force=True)
722 self._update_perspective_menu()
724 def _update_perspective_menu(self):
725 self._c['menu bar']._c['perspective'].update(
726 sorted(self._perspectives.keys()),
727 self.gui.config['active perspective'])
729 def _save_perspective(self, perspective, perspective_dir, name,
731 path = os.path.join(perspective_dir, name)
732 if extension != None:
734 if not os.path.isdir(perspective_dir):
735 os.makedirs(perspective_dir)
736 with open(path, 'w') as f:
738 self._perspectives[name] = perspective
739 self._restore_perspective(name)
740 self._update_perspective_menu()
742 def _delete_perspectives(self, perspective_dir, names,
744 self.log.debug('remove perspectives %s from %s'
745 % (names, perspective_dir))
747 path = os.path.join(perspective_dir, name)
748 if extension != None:
751 del(self._perspectives[name])
752 self._update_perspective_menu()
753 if self.gui.config['active perspective'] in names:
754 self._restore_perspective('Default')
755 # TODO: does this bug still apply?
756 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
757 # http://trac.wxwidgets.org/ticket/3258
758 # ) that makes the radio item indicator in the menu disappear.
759 # The code should be fine once this issue is fixed.
761 def _restore_perspective(self, name, force=False):
762 if name != self.gui.config['active perspective'] or force == True:
763 self.log.debug('restore perspective %s' % name)
764 self._set_config('active perspective', name)
765 self._c['manager'].LoadPerspective(self._perspectives[name])
766 self._c['manager'].Update()
767 for pane in self._c['manager'].GetAllPanes():
768 view = self._c['menu bar']._c['view']
769 if pane.name in view._c.keys():
770 view._c[pane.name].Check(pane.window.IsShown())
772 def _on_save_perspective(self, *args):
773 perspective = self._c['manager'].SavePerspective()
774 name = self.gui.config['active perspective']
775 if name == 'Default':
776 name = 'New perspective'
777 name = select_save_file(
778 directory=self.gui.config['perspective path'],
780 extension=self.gui.config['perspective extension'],
782 message='Enter a name for the new perspective:',
783 caption='Save perspective')
786 self._save_perspective(
787 perspective, self.gui.config['perspective path'], name=name,
788 extension=self.gui.config['perspective extension'])
790 def _on_delete_perspective(self, *args, **kwargs):
791 options = sorted([p for p in self._perspectives.keys()
793 dialog = SelectionDialog(
795 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
796 button_id=wx.ID_DELETE,
797 selection_style='multiple',
799 title='Delete perspective(s)',
800 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
801 dialog.CenterOnScreen()
803 if dialog.canceled == True:
805 names = [options[i] for i in dialog.selected]
807 self._delete_perspectives(
808 self.gui.config['perspective path'], names=names,
809 extension=self.gui.config['perspective extension'])
811 def _on_select_perspective(self, _class, method, name):
812 self._restore_perspective(name)
816 class HookeApp (wx.App):
817 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
819 Tosses up a splash screen and then loads :class:`HookeFrame` in
822 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
824 self.commands = commands
825 self.inqueue = inqueue
826 self.outqueue = outqueue
827 super(HookeApp, self).__init__(*args, **kwargs)
830 self.SetAppName('Hooke')
831 self.SetVendorName('')
832 self._setup_splash_screen()
834 height = self.gui.config['main height']
835 width = self.gui.config['main width']
836 top = self.gui.config['main top']
837 left = self.gui.config['main left']
839 # Sometimes, the ini file gets confused and sets 'left' and
840 # 'top' to large negative numbers. Here we catch and fix
841 # this. Keep small negative numbers, the user might want
850 self.gui, self.commands, self.inqueue, self.outqueue,
851 parent=None, title='Hooke',
852 pos=(left, top), size=(width, height),
853 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
855 self._c['frame'].Show(True)
856 self.SetTopWindow(self._c['frame'])
859 def _setup_splash_screen(self):
860 if self.gui.config['show splash screen'] == True:
861 path = self.gui.config['splash screen image']
862 if os.path.isfile(path):
863 duration = self.gui.config['splash screen duration']
865 bitmap=wx.Image(path).ConvertToBitmap(),
866 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
867 milliseconds=duration,
870 # For some reason splashDuration and sleep do not
871 # correspond to each other at least not on Windows.
872 # Maybe it's because duration is in milliseconds and
873 # sleep in seconds. Thus we need to increase the
874 # sleep time a bit. A factor of 1.2 seems to work.
876 time.sleep(sleepFactor * duration / 1000)
879 class GUI (UserInterface):
880 """wxWindows graphical user interface.
883 super(GUI, self).__init__(name='gui')
885 def default_settings(self):
886 """Return a list of :class:`hooke.config.Setting`\s for any
887 configurable UI settings.
889 The suggested section setting is::
891 Setting(section=self.setting_section, help=self.__doc__)
894 Setting(section=self.setting_section, help=self.__doc__),
895 Setting(section=self.setting_section, option='icon image',
896 value=os.path.join('doc', 'img', 'microscope.ico'),
898 help='Path to the hooke icon image.'),
899 Setting(section=self.setting_section, option='show splash screen',
900 value=True, type='bool',
901 help='Enable/disable the splash screen'),
902 Setting(section=self.setting_section, option='splash screen image',
903 value=os.path.join('doc', 'img', 'hooke.jpg'),
905 help='Path to the Hooke splash screen image.'),
906 Setting(section=self.setting_section,
907 option='splash screen duration',
908 value=1000, type='int',
909 help='Duration of the splash screen in milliseconds.'),
910 Setting(section=self.setting_section, option='perspective path',
911 value=os.path.join('resources', 'gui', 'perspective'),
912 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
913 Setting(section=self.setting_section, option='perspective extension',
915 help='Extension for perspective files.'),
916 Setting(section=self.setting_section, option='hide extensions',
917 value=False, type='bool',
918 help='Hide file extensions when displaying names.'),
919 Setting(section=self.setting_section, option='plot legend',
920 value=True, type='bool',
921 help='Enable/disable the plot legend.'),
922 Setting(section=self.setting_section, option='plot SI format',
923 value='True', type='bool',
924 help='Enable/disable SI plot axes numbering.'),
925 Setting(section=self.setting_section, option='plot decimals',
927 help='Number of decimal places to show if "plot SI format" is enabled.'),
928 Setting(section=self.setting_section, option='folders-workdir',
929 value='.', type='path',
930 help='This should probably go...'),
931 Setting(section=self.setting_section, option='folders-filters',
932 value='.', type='path',
933 help='This should probably go...'),
934 Setting(section=self.setting_section, option='active perspective',
936 help='Name of active perspective file (or "Default").'),
937 Setting(section=self.setting_section,
938 option='folders-filter-index',
940 help='This should probably go...'),
941 Setting(section=self.setting_section, option='main height',
942 value=450, type='int',
943 help='Height of main window in pixels.'),
944 Setting(section=self.setting_section, option='main width',
945 value=800, type='int',
946 help='Width of main window in pixels.'),
947 Setting(section=self.setting_section, option='main top',
949 help='Pixels from screen top to top of main window.'),
950 Setting(section=self.setting_section, option='main left',
952 help='Pixels from screen left to left of main window.'),
953 Setting(section=self.setting_section, option='selected command',
954 value='load playlist',
955 help='Name of the initially selected command.'),
958 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
962 app = HookeApp(gui=self,
964 inqueue=ui_to_command_queue,
965 outqueue=command_to_ui_queue,
969 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
970 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)