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_get_curve(self, command, args={}, results=[]):
471 """Update `self` to show the curve.
473 if not isinstance(results[-1], Success):
474 self._postprocess_text(command, results=results)
476 assert len(results) == 2, results
478 if args.get('curve', None) == None:
479 # the command defaults to the current curve of the current playlist
480 results = self.execute_command(
481 command=self._command_by_name('get playlist'))
482 playlist = results[0]
484 raise NotImplementedError()
485 if 'note' in self._c:
486 self._c['note'].set_text(curve.info.get('note', ''))
487 if 'playlist' in self._c:
488 self._c['playlist'].set_selected_curve(
490 if 'plot' in self._c:
491 self._c['plot'].set_curve(curve, config=self.gui.config)
493 def _postprocess_next_curve(self, command, args={}, results=[]):
494 """No-op. Only call 'next curve' via `self._next_curve()`.
498 def _postprocess_previous_curve(self, command, args={}, results=[]):
499 """No-op. Only call 'previous curve' via `self._previous_curve()`.
503 def _postprocess_glob_curves_to_playlist(
504 self, command, args={}, results=[]):
505 """Update `self` to show new curves.
507 if not isinstance(results[-1], Success):
508 self._postprocess_text(command, results=results)
510 if 'playlist' in self._c:
511 if args.get('playlist', None) != None:
512 playlist = args['playlist']
513 pname = playlist.name
514 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
515 assert loaded == True, loaded
516 for curve in results[:-1]:
517 self._c['playlist']._add_curve(pname, curve)
519 self.execute_command(
520 command=self._command_by_name('get playlist'))
522 def _postprocess_zero_block_surface_contact_point(
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'))
530 def _postprocess_add_block_force_array(
531 self, command, args={}, results=[]):
532 """Update the curve, since the available columns may have changed.
534 if isinstance(results[-1], Success):
535 self.execute_command(
536 command=self._command_by_name('get curve'))
540 # Command panel interface
542 def select_command(self, _class, method, command):
543 #self.select_plugin(plugin=command.plugin)
544 self._c['property editor'].clear()
545 self._c['property editor']._argument_from_label = {}
546 for argument in command.arguments:
547 if argument.name == 'help':
550 results = self.execute_command(
551 command=self._command_by_name('playlists'))
552 if not isinstance(results[-1], Success):
553 self._postprocess_text(command, results=results)
556 playlists = results[0]
558 results = self.execute_command(
559 command=self._command_by_name('playlist curves'))
560 if not isinstance(results[-1], Success):
561 self._postprocess_text(command, results=results)
566 ret = props_from_argument(
567 argument, curves=curves, playlists=playlists)
569 continue # property intentionally not handled (yet)
571 self._c['property editor'].append_property(p)
572 self._c['property editor']._argument_from_label[label] = (
575 self._set_config('selected command', command.name)
577 def select_plugin(self, _class=None, method=None, plugin=None):
582 # Folders panel interface
584 def _on_dir_ctrl_left_double_click(self, event):
585 file_path = self.panelFolders.GetPath()
586 if os.path.isfile(file_path):
587 if file_path.endswith('.hkp'):
588 self.do_loadlist(file_path)
593 # Note panel interface
595 def _on_update_note(self, _class, method, text):
596 """Sets the note for the active curve.
598 self.execute_command(
599 command=self._command_by_name('set note'),
604 # Playlist panel interface
606 def _on_user_delete_playlist(self, _class, method, playlist):
609 def _on_delete_playlist(self, _class, method, playlist):
610 if hasattr(playlist, 'path') and playlist.path != None:
611 os.remove(playlist.path)
613 def _on_user_delete_curve(self, _class, method, playlist, curve):
616 def _on_delete_curve(self, _class, method, playlist, curve):
617 # TODO: execute_command 'remove curve from playlist'
618 os.remove(curve.path)
620 def _on_set_selected_playlist(self, _class, method, playlist):
621 """Call the `jump to playlist` command.
623 results = self.execute_command(
624 command=self._command_by_name('playlists'))
625 if not isinstance(results[-1], Success):
627 assert len(results) == 2, results
628 playlists = results[0]
629 matching = [p for p in playlists if p.name == playlist.name]
630 assert len(matching) == 1, matching
631 index = playlists.index(matching[0])
632 results = self.execute_command(
633 command=self._command_by_name('jump to playlist'),
634 args={'index':index})
636 def _on_set_selected_curve(self, _class, method, playlist, curve):
637 """Call the `jump to curve` command.
639 self._on_set_selected_playlist(_class, method, playlist)
640 index = playlist.index(curve)
641 results = self.execute_command(
642 command=self._command_by_name('jump to curve'),
643 args={'index':index})
644 if not isinstance(results[-1], Success):
646 #results = self.execute_command(
647 # command=self._command_by_name('get playlist'))
648 #if not isinstance(results[-1], Success):
650 self.execute_command(
651 command=self._command_by_name('get curve'))
655 # Plot panel interface
657 def _on_plot_status_text(self, _class, method, text):
658 if 'status bar' in self._c:
659 self._c['status bar'].set_plot_text(text)
665 def _next_curve(self, *args):
666 """Call the `next curve` command.
668 results = self.execute_command(
669 command=self._command_by_name('next curve'))
670 if isinstance(results[-1], Success):
671 self.execute_command(
672 command=self._command_by_name('get curve'))
674 def _previous_curve(self, *args):
675 """Call the `previous curve` command.
677 results = self.execute_command(
678 command=self._command_by_name('previous curve'))
679 if isinstance(results[-1], Success):
680 self.execute_command(
681 command=self._command_by_name('get curve'))
685 # Panel display handling
687 def _on_pane_close(self, event):
689 view = self._c['menu bar']._c['view']
690 if pane.name in view._c.keys():
691 view._c[pane.name].Check(False)
694 def _on_panel_visibility(self, _class, method, panel_name, visible):
695 pane = self._c['manager'].GetPane(panel_name)
697 #if we don't do the following, the Folders pane does not resize properly on hide/show
698 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
699 #folders_size = pane.GetSize()
700 self.panelFolders.Fit()
701 self._c['manager'].Update()
703 def _setup_perspectives(self):
704 """Add perspectives to menubar and _perspectives.
706 self._perspectives = {
707 'Default': self._c['manager'].SavePerspective(),
709 path = os.path.expanduser(self.gui.config['perspective path'])
710 if os.path.isdir(path):
711 files = sorted(os.listdir(path))
713 name, extension = os.path.splitext(fname)
714 if extension != self.gui.config['perspective extension']:
716 fpath = os.path.join(path, fname)
717 if not os.path.isfile(fpath):
720 with open(fpath, 'rU') as f:
721 perspective = f.readline()
723 self._perspectives[name] = perspective
725 selected_perspective = self.gui.config['active perspective']
726 if not self._perspectives.has_key(selected_perspective):
727 self._set_config('active perspective', 'Default')
729 self._restore_perspective(selected_perspective, force=True)
730 self._update_perspective_menu()
732 def _update_perspective_menu(self):
733 self._c['menu bar']._c['perspective'].update(
734 sorted(self._perspectives.keys()),
735 self.gui.config['active perspective'])
737 def _save_perspective(self, perspective, perspective_dir, name,
739 path = os.path.join(perspective_dir, name)
740 if extension != None:
742 if not os.path.isdir(perspective_dir):
743 os.makedirs(perspective_dir)
744 with open(path, 'w') as f:
746 self._perspectives[name] = perspective
747 self._restore_perspective(name)
748 self._update_perspective_menu()
750 def _delete_perspectives(self, perspective_dir, names,
752 self.log.debug('remove perspectives %s from %s'
753 % (names, perspective_dir))
755 path = os.path.join(perspective_dir, name)
756 if extension != None:
759 del(self._perspectives[name])
760 self._update_perspective_menu()
761 if self.gui.config['active perspective'] in names:
762 self._restore_perspective('Default')
763 # TODO: does this bug still apply?
764 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
765 # http://trac.wxwidgets.org/ticket/3258
766 # ) that makes the radio item indicator in the menu disappear.
767 # The code should be fine once this issue is fixed.
769 def _restore_perspective(self, name, force=False):
770 if name != self.gui.config['active perspective'] or force == True:
771 self.log.debug('restore perspective %s' % name)
772 self._set_config('active perspective', name)
773 self._c['manager'].LoadPerspective(self._perspectives[name])
774 self._c['manager'].Update()
775 for pane in self._c['manager'].GetAllPanes():
776 view = self._c['menu bar']._c['view']
777 if pane.name in view._c.keys():
778 view._c[pane.name].Check(pane.window.IsShown())
780 def _on_save_perspective(self, *args):
781 perspective = self._c['manager'].SavePerspective()
782 name = self.gui.config['active perspective']
783 if name == 'Default':
784 name = 'New perspective'
785 name = select_save_file(
786 directory=os.path.expanduser(self.gui.config['perspective path']),
788 extension=self.gui.config['perspective extension'],
790 message='Enter a name for the new perspective:',
791 caption='Save perspective')
794 self._save_perspective(
796 os.path.expanduser(self.gui.config['perspective path']), name=name,
797 extension=self.gui.config['perspective extension'])
799 def _on_delete_perspective(self, *args, **kwargs):
800 options = sorted([p for p in self._perspectives.keys()
802 dialog = SelectionDialog(
804 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
805 button_id=wx.ID_DELETE,
806 selection_style='multiple',
808 title='Delete perspective(s)',
809 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
810 dialog.CenterOnScreen()
812 if dialog.canceled == True:
814 names = [options[i] for i in dialog.selected]
816 self._delete_perspectives(
817 os.path.expanduser(self.gui.config['perspective path']),
818 names=names, extension=self.gui.config['perspective extension'])
820 def _on_select_perspective(self, _class, method, name):
821 self._restore_perspective(name)
825 class HookeApp (wx.App):
826 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
828 Tosses up a splash screen and then loads :class:`HookeFrame` in
831 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
833 self.commands = commands
834 self.inqueue = inqueue
835 self.outqueue = outqueue
836 super(HookeApp, self).__init__(*args, **kwargs)
839 self.SetAppName('Hooke')
840 self.SetVendorName('')
841 self._setup_splash_screen()
843 height = self.gui.config['main height']
844 width = self.gui.config['main width']
845 top = self.gui.config['main top']
846 left = self.gui.config['main left']
848 # Sometimes, the ini file gets confused and sets 'left' and
849 # 'top' to large negative numbers. Here we catch and fix
850 # this. Keep small negative numbers, the user might want
859 self.gui, self.commands, self.inqueue, self.outqueue,
860 parent=None, title='Hooke',
861 pos=(left, top), size=(width, height),
862 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
864 self._c['frame'].Show(True)
865 self.SetTopWindow(self._c['frame'])
868 def _setup_splash_screen(self):
869 if self.gui.config['show splash screen'] == True:
870 path = os.path.expanduser(self.gui.config['splash screen image'])
871 if os.path.isfile(path):
872 duration = self.gui.config['splash screen duration']
874 bitmap=wx.Image(path).ConvertToBitmap(),
875 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
876 milliseconds=duration,
879 # For some reason splashDuration and sleep do not
880 # correspond to each other at least not on Windows.
881 # Maybe it's because duration is in milliseconds and
882 # sleep in seconds. Thus we need to increase the
883 # sleep time a bit. A factor of 1.2 seems to work.
885 time.sleep(sleepFactor * duration / 1000)
888 class GUI (UserInterface):
889 """wxWindows graphical user interface.
892 super(GUI, self).__init__(name='gui')
894 def default_settings(self):
895 """Return a list of :class:`hooke.config.Setting`\s for any
896 configurable UI settings.
898 The suggested section setting is::
900 Setting(section=self.setting_section, help=self.__doc__)
903 Setting(section=self.setting_section, help=self.__doc__),
904 Setting(section=self.setting_section, option='icon image',
905 value=os.path.join('doc', 'img', 'microscope.ico'),
907 help='Path to the hooke icon image.'),
908 Setting(section=self.setting_section, option='show splash screen',
909 value=True, type='bool',
910 help='Enable/disable the splash screen'),
911 Setting(section=self.setting_section, option='splash screen image',
912 value=os.path.join('doc', 'img', 'hooke.jpg'),
914 help='Path to the Hooke splash screen image.'),
915 Setting(section=self.setting_section,
916 option='splash screen duration',
917 value=1000, type='int',
918 help='Duration of the splash screen in milliseconds.'),
919 Setting(section=self.setting_section, option='perspective path',
920 value=os.path.join('resources', 'gui', 'perspective'),
921 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
922 Setting(section=self.setting_section, option='perspective extension',
924 help='Extension for perspective files.'),
925 Setting(section=self.setting_section, option='hide extensions',
926 value=False, type='bool',
927 help='Hide file extensions when displaying names.'),
928 Setting(section=self.setting_section, option='plot legend',
929 value=True, type='bool',
930 help='Enable/disable the plot legend.'),
931 Setting(section=self.setting_section, option='plot SI format',
932 value='True', type='bool',
933 help='Enable/disable SI plot axes numbering.'),
934 Setting(section=self.setting_section, option='plot decimals',
936 help='Number of decimal places to show if "plot SI format" is enabled.'),
937 Setting(section=self.setting_section, option='folders-workdir',
938 value='.', type='path',
939 help='This should probably go...'),
940 Setting(section=self.setting_section, option='folders-filters',
941 value='.', type='path',
942 help='This should probably go...'),
943 Setting(section=self.setting_section, option='active perspective',
945 help='Name of active perspective file (or "Default").'),
946 Setting(section=self.setting_section,
947 option='folders-filter-index',
949 help='This should probably go...'),
950 Setting(section=self.setting_section, option='main height',
951 value=450, type='int',
952 help='Height of main window in pixels.'),
953 Setting(section=self.setting_section, option='main width',
954 value=800, type='int',
955 help='Width of main window in pixels.'),
956 Setting(section=self.setting_section, option='main top',
958 help='Pixels from screen top to top of main window.'),
959 Setting(section=self.setting_section, option='main left',
961 help='Pixels from screen left to left of main window.'),
962 Setting(section=self.setting_section, option='selected command',
963 value='load playlist',
964 help='Name of the initially selected command.'),
967 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
971 app = HookeApp(gui=self,
973 inqueue=ui_to_command_queue,
974 outqueue=command_to_ui_queue,
978 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
979 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)