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),
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 explicit_execute_command(self, _class=None, method=None,
304 command=None, args=None):
305 return self.execute_command(
306 _class=_class, method=method, command=command, args=args,
307 explicit_user_call=True)
309 def execute_command(self, _class=None, method=None,
310 command=None, args=None, explicit_user_call=False):
313 if ('property editor' in self._c
314 and self.gui.config['selected command'] == command.name):
315 for name,value in self._c['property editor'].get_values().items():
316 arg = self._c['property editor']._argument_from_label.get(
321 args[arg.name] = value
323 # deal with counted arguments
324 if arg.name not in args:
326 index = int(name[len(arg.name):])
327 args[arg.name][index] = value
328 for arg in command.arguments:
329 if arg.name not in args:
330 continue # undisplayed argument, e.g. 'driver' types.
332 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
333 count = arg._display_count
334 if count != 1 and arg.name in args:
335 keys = sorted(args[arg.name].keys())
336 assert keys == range(count), keys
337 args[arg.name] = [args[arg.name][i]
338 for i in range(count)]
340 while (len(args[arg.name]) > 0
341 and args[arg.name][-1] == None):
343 if len(args[arg.name]) == 0:
344 args[arg.name] = arg.default
345 cm = CommandMessage(command.name, args)
346 self.gui._submit_command(
347 cm, self.inqueue, explicit_user_call=explicit_user_call)
348 # TODO: skip responses for commands that were captured by the
349 # command stack. We'd need to poll on each request, remember
350 # capture state, or add a flag to the response...
351 return self._handle_response(command_message=cm)
353 def _handle_response(self, command_message):
356 msg = self.outqueue.get()
358 if isinstance(msg, Exit):
361 elif isinstance(msg, CommandExit):
362 # TODO: display command complete
364 elif isinstance(msg, ReloadUserInterfaceConfig):
365 self.gui.reload_config(msg.config)
367 elif isinstance(msg, Request):
368 h = handler.HANDLERS[msg.type]
369 h.run(self, msg) # TODO: pause for response?
372 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
373 self._postprocess_text)
374 pp(command=command_message.command,
375 args=command_message.arguments,
379 def _handle_request(self, msg):
380 """Repeatedly try to get a response to `msg`.
383 raise NotImplementedError('_%s_request_prompt' % msg.type)
384 prompt_string = prompt(msg)
385 parser = getattr(self, '_%s_request_parser' % msg.type, None)
387 raise NotImplementedError('_%s_request_parser' % msg.type)
391 self.cmd.stdout.write(''.join([
392 error.__class__.__name__, ': ', str(error), '\n']))
393 self.cmd.stdout.write(prompt_string)
394 value = parser(msg, self.cmd.stdin.readline())
396 response = msg.response(value)
398 except ValueError, error:
400 self.inqueue.put(response)
402 def _set_config(self, option, value, section=None):
403 self.gui._set_config(section=section, option=option, value=value,
404 ui_to_command_queue=self.inqueue,
405 response_handler=self._handle_response)
408 # Command-specific postprocessing
410 def _postprocess_text(self, command, args={}, results=[]):
411 """Print the string representation of the results to the Results window.
413 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
414 approach, except that :class:`~hooke.ui.commandline.DoCommand`
415 doesn't print some internally handled messages
416 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
418 for result in results:
419 if isinstance(result, CommandExit):
420 self._c['output'].write(result.__class__.__name__+'\n')
421 self._c['output'].write(str(result).rstrip()+'\n')
423 def _postprocess_playlists(self, command, args={}, results=None):
424 """Update `self` to show the playlists.
426 if not isinstance(results[-1], Success):
427 self._postprocess_text(command, results=results)
429 assert len(results) == 2, results
430 playlists = results[0]
431 if 'playlist' in self._c:
432 for playlist in playlists:
433 if self._c['playlist'].is_playlist_loaded(playlist):
434 self._c['playlist'].update_playlist(playlist)
436 self._c['playlist'].add_playlist(playlist)
438 def _postprocess_new_playlist(self, command, args={}, results=None):
439 """Update `self` to show the new playlist.
441 if not isinstance(results[-1], Success):
442 self._postprocess_text(command, results=results)
444 assert len(results) == 2, results
445 playlist = results[0]
446 if 'playlist' in self._c:
447 loaded = self._c['playlist'].is_playlist_loaded(playlist)
448 assert loaded == False, loaded
449 self._c['playlist'].add_playlist(playlist)
451 def _postprocess_load_playlist(self, command, args={}, results=None):
452 """Update `self` to show the playlist.
454 if not isinstance(results[-1], Success):
455 self._postprocess_text(command, results=results)
457 assert len(results) == 2, results
458 playlist = results[0]
459 self._c['playlist'].add_playlist(playlist)
461 def _postprocess_get_playlist(self, command, args={}, results=[]):
462 if not isinstance(results[-1], Success):
463 self._postprocess_text(command, results=results)
465 assert len(results) == 2, results
466 playlist = results[0]
467 if 'playlist' in self._c:
468 loaded = self._c['playlist'].is_playlist_loaded(playlist)
469 assert loaded == True, loaded
470 self._c['playlist'].update_playlist(playlist)
472 def _postprocess_get_curve(self, command, args={}, results=[]):
473 """Update `self` to show the curve.
475 if not isinstance(results[-1], Success):
476 self._postprocess_text(command, results=results)
478 assert len(results) == 2, results
480 if args.get('curve', None) == None:
481 # the command defaults to the current curve of the current playlist
482 results = self.execute_command(
483 command=self._command_by_name('get playlist'))
484 playlist = results[0]
486 raise NotImplementedError()
487 if 'note' in self._c:
488 self._c['note'].set_text(curve.info.get('note', ''))
489 if 'playlist' in self._c:
490 self._c['playlist'].set_selected_curve(
492 if 'plot' in self._c:
493 self._c['plot'].set_curve(curve, config=self.gui.config)
495 def _postprocess_next_curve(self, command, args={}, results=[]):
496 """No-op. Only call 'next curve' via `self._next_curve()`.
500 def _postprocess_previous_curve(self, command, args={}, results=[]):
501 """No-op. Only call 'previous curve' via `self._previous_curve()`.
505 def _postprocess_glob_curves_to_playlist(
506 self, command, args={}, results=[]):
507 """Update `self` to show new curves.
509 if not isinstance(results[-1], Success):
510 self._postprocess_text(command, results=results)
512 if 'playlist' in self._c:
513 if args.get('playlist', None) != None:
514 playlist = args['playlist']
515 pname = playlist.name
516 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
517 assert loaded == True, loaded
518 for curve in results[:-1]:
519 self._c['playlist']._add_curve(pname, curve)
521 self.execute_command(
522 command=self._command_by_name('get playlist'))
524 def _postprocess_zero_block_surface_contact_point(
525 self, command, args={}, results=[]):
526 """Update the curve, since the available columns may have changed.
528 if isinstance(results[-1], Success):
529 self.execute_command(
530 command=self._command_by_name('get curve'))
532 def _postprocess_add_block_force_array(
533 self, command, args={}, results=[]):
534 """Update the curve, since the available columns may have changed.
536 if isinstance(results[-1], Success):
537 self.execute_command(
538 command=self._command_by_name('get curve'))
542 # Command panel interface
544 def select_command(self, _class, method, command):
545 #self.select_plugin(plugin=command.plugin)
546 self._c['property editor'].clear()
547 self._c['property editor']._argument_from_label = {}
548 for argument in command.arguments:
549 if argument.name == 'help':
552 results = self.execute_command(
553 command=self._command_by_name('playlists'))
554 if not isinstance(results[-1], Success):
555 self._postprocess_text(command, results=results)
558 playlists = results[0]
560 results = self.execute_command(
561 command=self._command_by_name('playlist curves'))
562 if not isinstance(results[-1], Success):
563 self._postprocess_text(command, results=results)
568 ret = props_from_argument(
569 argument, curves=curves, playlists=playlists)
571 continue # property intentionally not handled (yet)
573 self._c['property editor'].append_property(p)
574 self._c['property editor']._argument_from_label[label] = (
577 self._set_config('selected command', command.name)
579 def select_plugin(self, _class=None, method=None, plugin=None):
584 # Folders panel interface
586 def _on_dir_ctrl_left_double_click(self, event):
587 file_path = self.panelFolders.GetPath()
588 if os.path.isfile(file_path):
589 if file_path.endswith('.hkp'):
590 self.do_loadlist(file_path)
595 # Note panel interface
597 def _on_update_note(self, _class, method, text):
598 """Sets the note for the active curve.
600 self.execute_command(
601 command=self._command_by_name('set note'),
606 # Playlist panel interface
608 def _on_user_delete_playlist(self, _class, method, playlist):
611 def _on_delete_playlist(self, _class, method, playlist):
612 if hasattr(playlist, 'path') and playlist.path != None:
613 os.remove(playlist.path)
615 def _on_user_delete_curve(self, _class, method, playlist, curve):
618 def _on_delete_curve(self, _class, method, playlist, curve):
619 # TODO: execute_command 'remove curve from playlist'
620 os.remove(curve.path)
622 def _on_set_selected_playlist(self, _class, method, playlist):
623 """Call the `jump to playlist` command.
625 results = self.execute_command(
626 command=self._command_by_name('playlists'))
627 if not isinstance(results[-1], Success):
629 assert len(results) == 2, results
630 playlists = results[0]
631 matching = [p for p in playlists if p.name == playlist.name]
632 assert len(matching) == 1, matching
633 index = playlists.index(matching[0])
634 results = self.execute_command(
635 command=self._command_by_name('jump to playlist'),
636 args={'index':index})
638 def _on_set_selected_curve(self, _class, method, playlist, curve):
639 """Call the `jump to curve` command.
641 self._on_set_selected_playlist(_class, method, playlist)
642 index = playlist.index(curve)
643 results = self.execute_command(
644 command=self._command_by_name('jump to curve'),
645 args={'index':index})
646 if not isinstance(results[-1], Success):
648 #results = self.execute_command(
649 # command=self._command_by_name('get playlist'))
650 #if not isinstance(results[-1], Success):
652 self.execute_command(
653 command=self._command_by_name('get curve'))
657 # Plot panel interface
659 def _on_plot_status_text(self, _class, method, text):
660 if 'status bar' in self._c:
661 self._c['status bar'].set_plot_text(text)
667 def _next_curve(self, *args):
668 """Call the `next curve` command.
670 results = self.execute_command(
671 command=self._command_by_name('next curve'))
672 if isinstance(results[-1], Success):
673 self.execute_command(
674 command=self._command_by_name('get curve'))
676 def _previous_curve(self, *args):
677 """Call the `previous curve` command.
679 results = self.execute_command(
680 command=self._command_by_name('previous curve'))
681 if isinstance(results[-1], Success):
682 self.execute_command(
683 command=self._command_by_name('get curve'))
687 # Panel display handling
689 def _on_pane_close(self, event):
691 view = self._c['menu bar']._c['view']
692 if pane.name in view._c.keys():
693 view._c[pane.name].Check(False)
696 def _on_panel_visibility(self, _class, method, panel_name, visible):
697 pane = self._c['manager'].GetPane(panel_name)
699 #if we don't do the following, the Folders pane does not resize properly on hide/show
700 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
701 #folders_size = pane.GetSize()
702 self.panelFolders.Fit()
703 self._c['manager'].Update()
705 def _setup_perspectives(self):
706 """Add perspectives to menubar and _perspectives.
708 self._perspectives = {
709 'Default': self._c['manager'].SavePerspective(),
711 path = self.gui.config['perspective path']
712 if os.path.isdir(path):
713 files = sorted(os.listdir(path))
715 name, extension = os.path.splitext(fname)
716 if extension != self.gui.config['perspective extension']:
718 fpath = os.path.join(path, fname)
719 if not os.path.isfile(fpath):
722 with open(fpath, 'rU') as f:
723 perspective = f.readline()
725 self._perspectives[name] = perspective
727 selected_perspective = self.gui.config['active perspective']
728 if not self._perspectives.has_key(selected_perspective):
729 self._set_config('active perspective', 'Default')
731 self._restore_perspective(selected_perspective, force=True)
732 self._update_perspective_menu()
734 def _update_perspective_menu(self):
735 self._c['menu bar']._c['perspective'].update(
736 sorted(self._perspectives.keys()),
737 self.gui.config['active perspective'])
739 def _save_perspective(self, perspective, perspective_dir, name,
741 path = os.path.join(perspective_dir, name)
742 if extension != None:
744 if not os.path.isdir(perspective_dir):
745 os.makedirs(perspective_dir)
746 with open(path, 'w') as f:
748 self._perspectives[name] = perspective
749 self._restore_perspective(name)
750 self._update_perspective_menu()
752 def _delete_perspectives(self, perspective_dir, names,
754 self.log.debug('remove perspectives %s from %s'
755 % (names, perspective_dir))
757 path = os.path.join(perspective_dir, name)
758 if extension != None:
761 del(self._perspectives[name])
762 self._update_perspective_menu()
763 if self.gui.config['active perspective'] in names:
764 self._restore_perspective('Default')
765 # TODO: does this bug still apply?
766 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
767 # http://trac.wxwidgets.org/ticket/3258
768 # ) that makes the radio item indicator in the menu disappear.
769 # The code should be fine once this issue is fixed.
771 def _restore_perspective(self, name, force=False):
772 if name != self.gui.config['active perspective'] or force == True:
773 self.log.debug('restore perspective %s' % name)
774 self._set_config('active perspective', name)
775 self._c['manager'].LoadPerspective(self._perspectives[name])
776 self._c['manager'].Update()
777 for pane in self._c['manager'].GetAllPanes():
778 view = self._c['menu bar']._c['view']
779 if pane.name in view._c.keys():
780 view._c[pane.name].Check(pane.window.IsShown())
782 def _on_save_perspective(self, *args):
783 perspective = self._c['manager'].SavePerspective()
784 name = self.gui.config['active perspective']
785 if name == 'Default':
786 name = 'New perspective'
787 name = select_save_file(
788 directory=self.gui.config['perspective path'],
790 extension=self.gui.config['perspective extension'],
792 message='Enter a name for the new perspective:',
793 caption='Save perspective')
796 self._save_perspective(
797 perspective, self.gui.config['perspective path'], name=name,
798 extension=self.gui.config['perspective extension'])
800 def _on_delete_perspective(self, *args, **kwargs):
801 options = sorted([p for p in self._perspectives.keys()
803 dialog = SelectionDialog(
805 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
806 button_id=wx.ID_DELETE,
807 selection_style='multiple',
809 title='Delete perspective(s)',
810 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
811 dialog.CenterOnScreen()
813 if dialog.canceled == True:
815 names = [options[i] for i in dialog.selected]
817 self._delete_perspectives(
818 self.gui.config['perspective path'], names=names,
819 extension=self.gui.config['perspective extension'])
821 def _on_select_perspective(self, _class, method, name):
822 self._restore_perspective(name)
826 class HookeApp (wx.App):
827 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
829 Tosses up a splash screen and then loads :class:`HookeFrame` in
832 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
834 self.commands = commands
835 self.inqueue = inqueue
836 self.outqueue = outqueue
837 super(HookeApp, self).__init__(*args, **kwargs)
840 self.SetAppName('Hooke')
841 self.SetVendorName('')
842 self._setup_splash_screen()
844 height = self.gui.config['main height']
845 width = self.gui.config['main width']
846 top = self.gui.config['main top']
847 left = self.gui.config['main left']
849 # Sometimes, the ini file gets confused and sets 'left' and
850 # 'top' to large negative numbers. Here we catch and fix
851 # this. Keep small negative numbers, the user might want
860 self.gui, self.commands, self.inqueue, self.outqueue,
861 parent=None, title='Hooke',
862 pos=(left, top), size=(width, height),
863 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
865 self._c['frame'].Show(True)
866 self.SetTopWindow(self._c['frame'])
869 def _setup_splash_screen(self):
870 if self.gui.config['show splash screen'] == True:
871 path = self.gui.config['splash screen image']
872 if os.path.isfile(path):
873 duration = self.gui.config['splash screen duration']
875 bitmap=wx.Image(path).ConvertToBitmap(),
876 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
877 milliseconds=duration,
880 # For some reason splashDuration and sleep do not
881 # correspond to each other at least not on Windows.
882 # Maybe it's because duration is in milliseconds and
883 # sleep in seconds. Thus we need to increase the
884 # sleep time a bit. A factor of 1.2 seems to work.
886 time.sleep(sleepFactor * duration / 1000)
889 class GUI (UserInterface):
890 """wxWindows graphical user interface.
893 super(GUI, self).__init__(name='gui')
895 def default_settings(self):
896 """Return a list of :class:`hooke.config.Setting`\s for any
897 configurable UI settings.
899 The suggested section setting is::
901 Setting(section=self.setting_section, help=self.__doc__)
904 Setting(section=self.setting_section, help=self.__doc__),
905 Setting(section=self.setting_section, option='icon image',
906 value=os.path.join('doc', 'img', 'microscope.ico'),
908 help='Path to the hooke icon image.'),
909 Setting(section=self.setting_section, option='show splash screen',
910 value=True, type='bool',
911 help='Enable/disable the splash screen'),
912 Setting(section=self.setting_section, option='splash screen image',
913 value=os.path.join('doc', 'img', 'hooke.jpg'),
915 help='Path to the Hooke splash screen image.'),
916 Setting(section=self.setting_section,
917 option='splash screen duration',
918 value=1000, type='int',
919 help='Duration of the splash screen in milliseconds.'),
920 Setting(section=self.setting_section, option='perspective path',
921 value=os.path.join('resources', 'gui', 'perspective'),
922 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
923 Setting(section=self.setting_section, option='perspective extension',
925 help='Extension for perspective files.'),
926 Setting(section=self.setting_section, option='hide extensions',
927 value=False, type='bool',
928 help='Hide file extensions when displaying names.'),
929 Setting(section=self.setting_section, option='plot legend',
930 value=True, type='bool',
931 help='Enable/disable the plot legend.'),
932 Setting(section=self.setting_section, option='plot SI format',
933 value='True', type='bool',
934 help='Enable/disable SI plot axes numbering.'),
935 Setting(section=self.setting_section, option='plot decimals',
937 help='Number of decimal places to show if "plot SI format" is enabled.'),
938 Setting(section=self.setting_section, option='folders-workdir',
939 value='.', type='path',
940 help='This should probably go...'),
941 Setting(section=self.setting_section, option='folders-filters',
942 value='.', type='path',
943 help='This should probably go...'),
944 Setting(section=self.setting_section, option='active perspective',
946 help='Name of active perspective file (or "Default").'),
947 Setting(section=self.setting_section,
948 option='folders-filter-index',
950 help='This should probably go...'),
951 Setting(section=self.setting_section, option='main height',
952 value=450, type='int',
953 help='Height of main window in pixels.'),
954 Setting(section=self.setting_section, option='main width',
955 value=800, type='int',
956 help='Width of main window in pixels.'),
957 Setting(section=self.setting_section, option='main top',
959 help='Pixels from screen top to top of main window.'),
960 Setting(section=self.setting_section, option='main left',
962 help='Pixels from screen left to left of main window.'),
963 Setting(section=self.setting_section, option='selected command',
964 value='load playlist',
965 help='Name of the initially selected command.'),
968 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
972 app = HookeApp(gui=self,
974 inqueue=ui_to_command_queue,
975 outqueue=command_to_ui_queue,
979 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
980 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)