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.explicit_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),
200 self._add_panel(p, style)
201 self.execute_command( # setup already loaded playlists
202 command=self._command_by_name('playlists'))
203 self.execute_command( # setup already loaded curve
204 command=self._command_by_name('get curve'))
206 def _add_panel(self, panel, style):
207 self._c[panel.name] = panel
208 m_name = panel.managed_name
209 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
210 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
213 elif style == 'center':
215 elif style == 'left':
217 elif style == 'right':
220 assert style == 'bottom', style
222 self._c['manager'].AddPane(panel, info)
224 def _setup_toolbars(self):
225 self._c['navigation bar'] = navbar.NavBar(
227 'next': self._next_curve,
228 'previous': self._previous_curve,
231 style=wx.TB_FLAT | wx.TB_NODIVIDER)
232 self._c['manager'].AddPane(
233 self._c['navigation bar'],
234 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
235 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
236 ).RightDockable(False))
238 def _bind_events(self):
239 # TODO: figure out if we can use the eventManager for menu
240 # ranges and events of 'self' without raising an assertion
242 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
243 self.Bind(wx.EVT_SIZE, self._on_size)
244 self.Bind(wx.EVT_CLOSE, self._on_close)
245 self.Bind(aui.EVT_AUI_PANE_CLOSE, self._on_pane_close)
247 return # TODO: cleanup
248 treeCtrl = self._c['folders'].GetTreeCtrl()
249 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
251 def _on_about(self, *args):
252 dialog = wx.MessageDialog(
254 message=self.gui._splash_text(extra_info={
255 'get-details':'click "Help -> License"'},
257 caption='About Hooke',
258 style=wx.OK|wx.ICON_INFORMATION)
262 def _on_size(self, event):
265 def _on_close(self, *args):
266 self.log.info('closing GUI framework')
268 self._set_config('main height', self.GetSize().GetHeight())
269 self._set_config('main left', self.GetPosition()[0])
270 self._set_config('main top', self.GetPosition()[1])
271 self._set_config('main width', self.GetSize().GetWidth())
272 self._c['manager'].UnInit()
273 del self._c['manager']
276 def _on_erase_background(self, event):
281 # Panel utility functions
283 def _file_name(self, name):
284 """Cleanup names according to configured preferences.
286 if self.gui.config['hide extensions'] == True:
287 name,ext = os.path.splitext(name)
294 def _command_by_name(self, name):
295 cs = [c for c in self.commands if c.name == name]
299 raise Exception('Multiple commands named "%s"' % name)
302 def explicit_execute_command(self, _class=None, method=None,
303 command=None, args=None):
304 return self.execute_command(
305 _class=_class, method=method, command=command, args=args,
306 explicit_user_call=True)
308 def execute_command(self, _class=None, method=None,
309 command=None, args=None, explicit_user_call=False):
312 if ('property editor' in self._c
313 and self.gui.config['selected command'] == command.name):
314 for name,value in self._c['property editor'].get_values().items():
315 arg = self._c['property editor']._argument_from_label.get(
320 args[arg.name] = value
322 # deal with counted arguments
323 if arg.name not in args:
325 index = int(name[len(arg.name):])
326 args[arg.name][index] = value
327 for arg in command.arguments:
328 if arg.name not in args:
329 continue # undisplayed argument, e.g. 'driver' types.
331 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
332 count = arg._display_count
333 if count != 1 and arg.name in args:
334 keys = sorted(args[arg.name].keys())
335 assert keys == range(count), keys
336 args[arg.name] = [args[arg.name][i]
337 for i in range(count)]
339 while (len(args[arg.name]) > 0
340 and args[arg.name][-1] == None):
342 if len(args[arg.name]) == 0:
343 args[arg.name] = arg.default
344 cm = CommandMessage(command.name, args)
345 self.gui._submit_command(
346 cm, self.inqueue, explicit_user_call=explicit_user_call)
347 # TODO: skip responses for commands that were captured by the
348 # command stack. We'd need to poll on each request, remember
349 # capture state, or add a flag to the response...
350 return self._handle_response(command_message=cm)
352 def _handle_response(self, command_message):
355 msg = self.outqueue.get()
357 if isinstance(msg, Exit):
360 elif isinstance(msg, CommandExit):
361 # TODO: display command complete
363 elif isinstance(msg, ReloadUserInterfaceConfig):
364 self.gui.reload_config(msg.config)
366 elif isinstance(msg, Request):
367 h = handler.HANDLERS[msg.type]
368 h.run(self, msg) # TODO: pause for response?
371 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
372 self._postprocess_text)
373 pp(command=command_message.command,
374 args=command_message.arguments,
378 def _handle_request(self, msg):
379 """Repeatedly try to get a response to `msg`.
382 raise NotImplementedError('_%s_request_prompt' % msg.type)
383 prompt_string = prompt(msg)
384 parser = getattr(self, '_%s_request_parser' % msg.type, None)
386 raise NotImplementedError('_%s_request_parser' % msg.type)
390 self.cmd.stdout.write(''.join([
391 error.__class__.__name__, ': ', str(error), '\n']))
392 self.cmd.stdout.write(prompt_string)
393 value = parser(msg, self.cmd.stdin.readline())
395 response = msg.response(value)
397 except ValueError, error:
399 self.inqueue.put(response)
401 def _set_config(self, option, value, section=None):
402 self.gui._set_config(section=section, option=option, value=value,
403 ui_to_command_queue=self.inqueue,
404 response_handler=self._handle_response)
407 # Command-specific postprocessing
409 def _postprocess_text(self, command, args={}, results=[]):
410 """Print the string representation of the results to the Results window.
412 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
413 approach, except that :class:`~hooke.ui.commandline.DoCommand`
414 doesn't print some internally handled messages
415 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
417 for result in results:
418 if isinstance(result, CommandExit):
419 self._c['output'].write(result.__class__.__name__+'\n')
420 self._c['output'].write(str(result).rstrip()+'\n')
422 def _postprocess_playlists(self, command, args={}, results=None):
423 """Update `self` to show the playlists.
425 if not isinstance(results[-1], Success):
426 self._postprocess_text(command, results=results)
428 assert len(results) == 2, results
429 playlists = results[0]
430 if 'playlist' in self._c:
431 for playlist in playlists:
432 if self._c['playlist'].is_playlist_loaded(playlist):
433 self._c['playlist'].update_playlist(playlist)
435 self._c['playlist'].add_playlist(playlist)
437 def _postprocess_new_playlist(self, command, args={}, results=None):
438 """Update `self` to show the new playlist.
440 if not isinstance(results[-1], Success):
441 self._postprocess_text(command, results=results)
443 assert len(results) == 2, results
444 playlist = results[0]
445 if 'playlist' in self._c:
446 loaded = self._c['playlist'].is_playlist_loaded(playlist)
447 assert loaded == False, loaded
448 self._c['playlist'].add_playlist(playlist)
450 def _postprocess_load_playlist(self, command, args={}, results=None):
451 """Update `self` to show the playlist.
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 self._c['playlist'].add_playlist(playlist)
460 def _postprocess_get_playlist(self, command, args={}, results=[]):
461 if not isinstance(results[-1], Success):
462 self._postprocess_text(command, results=results)
464 assert len(results) == 2, results
465 playlist = results[0]
466 if 'playlist' in self._c:
467 loaded = self._c['playlist'].is_playlist_loaded(playlist)
468 assert loaded == True, loaded
469 self._c['playlist'].update_playlist(playlist)
471 def _postprocess_get_curve(self, command, args={}, results=[]):
472 """Update `self` to show the curve.
474 if not isinstance(results[-1], Success):
475 self._postprocess_text(command, results=results)
477 assert len(results) == 2, results
479 if args.get('curve', None) == None:
480 # the command defaults to the current curve of the current playlist
481 results = self.execute_command(
482 command=self._command_by_name('get playlist'))
483 playlist = results[0]
485 raise NotImplementedError()
486 if 'note' in self._c:
487 self._c['note'].set_text(curve.info.get('note', ''))
488 if 'playlist' in self._c:
489 self._c['playlist'].set_selected_curve(
491 if 'plot' in self._c:
492 self._c['plot'].set_curve(curve, config=self.gui.config)
494 def _postprocess_next_curve(self, command, args={}, results=[]):
495 """No-op. Only call 'next curve' via `self._next_curve()`.
499 def _postprocess_previous_curve(self, command, args={}, results=[]):
500 """No-op. Only call 'previous curve' via `self._previous_curve()`.
504 def _postprocess_glob_curves_to_playlist(
505 self, command, args={}, results=[]):
506 """Update `self` to show new curves.
508 if not isinstance(results[-1], Success):
509 self._postprocess_text(command, results=results)
511 if 'playlist' in self._c:
512 if args.get('playlist', None) != None:
513 playlist = args['playlist']
514 pname = playlist.name
515 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
516 assert loaded == True, loaded
517 for curve in results[:-1]:
518 self._c['playlist']._add_curve(pname, curve)
520 self.execute_command(
521 command=self._command_by_name('get playlist'))
523 def _postprocess_zero_block_surface_contact_point(
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'))
531 def _postprocess_add_block_force_array(
532 self, command, args={}, results=[]):
533 """Update the curve, since the available columns may have changed.
535 if isinstance(results[-1], Success):
536 self.execute_command(
537 command=self._command_by_name('get curve'))
541 # Command panel interface
543 def select_command(self, _class, method, command):
544 #self.select_plugin(plugin=command.plugin)
545 self._c['property editor'].clear()
546 self._c['property editor']._argument_from_label = {}
547 for argument in command.arguments:
548 if argument.name == 'help':
551 results = self.execute_command(
552 command=self._command_by_name('playlists'))
553 if not isinstance(results[-1], Success):
554 self._postprocess_text(command, results=results)
557 playlists = results[0]
559 results = self.execute_command(
560 command=self._command_by_name('playlist curves'))
561 if not isinstance(results[-1], Success):
562 self._postprocess_text(command, results=results)
567 ret = props_from_argument(
568 argument, curves=curves, playlists=playlists)
570 continue # property intentionally not handled (yet)
572 self._c['property editor'].append_property(p)
573 self._c['property editor']._argument_from_label[label] = (
576 self._set_config('selected command', command.name)
578 def select_plugin(self, _class=None, method=None, plugin=None):
583 # Folders panel interface
585 def _on_dir_ctrl_left_double_click(self, event):
586 file_path = self.panelFolders.GetPath()
587 if os.path.isfile(file_path):
588 if file_path.endswith('.hkp'):
589 self.do_loadlist(file_path)
594 # Note panel interface
596 def _on_update_note(self, _class, method, text):
597 """Sets the note for the active curve.
599 self.execute_command(
600 command=self._command_by_name('set note'),
605 # Playlist panel interface
607 def _on_user_delete_playlist(self, _class, method, playlist):
610 def _on_delete_playlist(self, _class, method, playlist):
611 if hasattr(playlist, 'path') and playlist.path != None:
612 os.remove(playlist.path)
614 def _on_user_delete_curve(self, _class, method, playlist, curve):
617 def _on_delete_curve(self, _class, method, playlist, curve):
618 # TODO: execute_command 'remove curve from playlist'
619 os.remove(curve.path)
621 def _on_set_selected_playlist(self, _class, method, playlist):
622 """Call the `jump to playlist` command.
624 results = self.execute_command(
625 command=self._command_by_name('playlists'))
626 if not isinstance(results[-1], Success):
628 assert len(results) == 2, results
629 playlists = results[0]
630 matching = [p for p in playlists if p.name == playlist.name]
631 assert len(matching) == 1, matching
632 index = playlists.index(matching[0])
633 results = self.execute_command(
634 command=self._command_by_name('jump to playlist'),
635 args={'index':index})
637 def _on_set_selected_curve(self, _class, method, playlist, curve):
638 """Call the `jump to curve` command.
640 self._on_set_selected_playlist(_class, method, playlist)
641 index = playlist.index(curve)
642 results = self.execute_command(
643 command=self._command_by_name('jump to curve'),
644 args={'index':index})
645 if not isinstance(results[-1], Success):
647 #results = self.execute_command(
648 # command=self._command_by_name('get playlist'))
649 #if not isinstance(results[-1], Success):
651 self.execute_command(
652 command=self._command_by_name('get curve'))
656 # Plot panel interface
658 def _on_plot_status_text(self, _class, method, text):
659 if 'status bar' in self._c:
660 self._c['status bar'].set_plot_text(text)
666 def _next_curve(self, *args):
667 """Call the `next curve` command.
669 results = self.execute_command(
670 command=self._command_by_name('next curve'))
671 if isinstance(results[-1], Success):
672 self.execute_command(
673 command=self._command_by_name('get curve'))
675 def _previous_curve(self, *args):
676 """Call the `previous curve` command.
678 results = self.execute_command(
679 command=self._command_by_name('previous curve'))
680 if isinstance(results[-1], Success):
681 self.execute_command(
682 command=self._command_by_name('get curve'))
686 # Panel display handling
688 def _on_pane_close(self, event):
690 view = self._c['menu bar']._c['view']
691 if pane.name in view._c.keys():
692 view._c[pane.name].Check(False)
695 def _on_panel_visibility(self, _class, method, panel_name, visible):
696 pane = self._c['manager'].GetPane(panel_name)
698 #if we don't do the following, the Folders pane does not resize properly on hide/show
699 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
700 #folders_size = pane.GetSize()
701 self.panelFolders.Fit()
702 self._c['manager'].Update()
704 def _setup_perspectives(self):
705 """Add perspectives to menubar and _perspectives.
707 self._perspectives = {
708 'Default': self._c['manager'].SavePerspective(),
710 path = self.gui.config['perspective path']
711 if os.path.isdir(path):
712 files = sorted(os.listdir(path))
714 name, extension = os.path.splitext(fname)
715 if extension != self.gui.config['perspective extension']:
717 fpath = os.path.join(path, fname)
718 if not os.path.isfile(fpath):
721 with open(fpath, 'rU') as f:
722 perspective = f.readline()
724 self._perspectives[name] = perspective
726 selected_perspective = self.gui.config['active perspective']
727 if not self._perspectives.has_key(selected_perspective):
728 self._set_config('active perspective', 'Default')
730 self._restore_perspective(selected_perspective, force=True)
731 self._update_perspective_menu()
733 def _update_perspective_menu(self):
734 self._c['menu bar']._c['perspective'].update(
735 sorted(self._perspectives.keys()),
736 self.gui.config['active perspective'])
738 def _save_perspective(self, perspective, perspective_dir, name,
740 path = os.path.join(perspective_dir, name)
741 if extension != None:
743 if not os.path.isdir(perspective_dir):
744 os.makedirs(perspective_dir)
745 with open(path, 'w') as f:
747 self._perspectives[name] = perspective
748 self._restore_perspective(name)
749 self._update_perspective_menu()
751 def _delete_perspectives(self, perspective_dir, names,
753 self.log.debug('remove perspectives %s from %s'
754 % (names, perspective_dir))
756 path = os.path.join(perspective_dir, name)
757 if extension != None:
760 del(self._perspectives[name])
761 self._update_perspective_menu()
762 if self.gui.config['active perspective'] in names:
763 self._restore_perspective('Default')
764 # TODO: does this bug still apply?
765 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
766 # http://trac.wxwidgets.org/ticket/3258
767 # ) that makes the radio item indicator in the menu disappear.
768 # The code should be fine once this issue is fixed.
770 def _restore_perspective(self, name, force=False):
771 if name != self.gui.config['active perspective'] or force == True:
772 self.log.debug('restore perspective %s' % name)
773 self._set_config('active perspective', name)
774 self._c['manager'].LoadPerspective(self._perspectives[name])
775 self._c['manager'].Update()
776 for pane in self._c['manager'].GetAllPanes():
777 view = self._c['menu bar']._c['view']
778 if pane.name in view._c.keys():
779 view._c[pane.name].Check(pane.window.IsShown())
781 def _on_save_perspective(self, *args):
782 perspective = self._c['manager'].SavePerspective()
783 name = self.gui.config['active perspective']
784 if name == 'Default':
785 name = 'New perspective'
786 name = select_save_file(
787 directory=self.gui.config['perspective path'],
789 extension=self.gui.config['perspective extension'],
791 message='Enter a name for the new perspective:',
792 caption='Save perspective')
795 self._save_perspective(
796 perspective, 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 self.gui.config['perspective path'], names=names,
818 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 = 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)