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_name_playlist(self, command, args={}, results=None):
471 """Update `self` to show the new playlist.
473 return self._postprocess_new_playlist(command, args, results)
475 def _postprocess_get_curve(self, command, args={}, results=[]):
476 """Update `self` to show the curve.
478 if not isinstance(results[-1], Success):
479 self._postprocess_text(command, results=results)
481 assert len(results) == 2, results
483 if args.get('curve', None) == None:
484 # the command defaults to the current curve of the current playlist
485 results = self.execute_command(
486 command=self._command_by_name('get playlist'))
487 playlist = results[0]
489 raise NotImplementedError()
490 if 'note' in self._c:
491 self._c['note'].set_text(curve.info.get('note', ''))
492 if 'playlist' in self._c:
493 self._c['playlist'].set_selected_curve(
495 if 'plot' in self._c:
496 self._c['plot'].set_curve(curve, config=self.gui.config)
498 def _postprocess_next_curve(self, command, args={}, results=[]):
499 """No-op. Only call 'next curve' via `self._next_curve()`.
503 def _postprocess_previous_curve(self, command, args={}, results=[]):
504 """No-op. Only call 'previous curve' via `self._previous_curve()`.
508 def _postprocess_glob_curves_to_playlist(
509 self, command, args={}, results=[]):
510 """Update `self` to show new curves.
512 if not isinstance(results[-1], Success):
513 self._postprocess_text(command, results=results)
515 if 'playlist' in self._c:
516 if args.get('playlist', None) != None:
517 playlist = args['playlist']
518 pname = playlist.name
519 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
520 assert loaded == True, loaded
521 for curve in results[:-1]:
522 self._c['playlist']._add_curve(pname, curve)
524 self.execute_command(
525 command=self._command_by_name('get playlist'))
527 def _update_curve(self, command, args={}, results=[]):
528 """Update the curve, since the available columns may have changed.
530 if isinstance(results[-1], Success):
531 self.execute_command(
532 command=self._command_by_name('get curve'))
535 # Command panel interface
537 def select_command(self, _class, method, command):
538 #self.select_plugin(plugin=command.plugin)
539 self._c['property editor'].clear()
540 self._c['property editor']._argument_from_label = {}
541 for argument in command.arguments:
542 if argument.name == 'help':
545 results = self.execute_command(
546 command=self._command_by_name('playlists'))
547 if not isinstance(results[-1], Success):
548 self._postprocess_text(command, results=results)
551 playlists = results[0]
553 results = self.execute_command(
554 command=self._command_by_name('playlist curves'))
555 if not isinstance(results[-1], Success):
556 self._postprocess_text(command, results=results)
561 ret = props_from_argument(
562 argument, curves=curves, playlists=playlists)
564 continue # property intentionally not handled (yet)
566 self._c['property editor'].append_property(p)
567 self._c['property editor']._argument_from_label[label] = (
570 self._set_config('selected command', command.name)
572 def select_plugin(self, _class=None, method=None, plugin=None):
577 # Folders panel interface
579 def _on_dir_ctrl_left_double_click(self, event):
580 file_path = self.panelFolders.GetPath()
581 if os.path.isfile(file_path):
582 if file_path.endswith('.hkp'):
583 self.do_loadlist(file_path)
588 # Note panel interface
590 def _on_update_note(self, _class, method, text):
591 """Sets the note for the active curve.
593 self.execute_command(
594 command=self._command_by_name('set note'),
599 # Playlist panel interface
601 def _on_user_delete_playlist(self, _class, method, playlist):
604 def _on_delete_playlist(self, _class, method, playlist):
605 if hasattr(playlist, 'path') and playlist.path != None:
606 os.remove(playlist.path)
608 def _on_user_delete_curve(self, _class, method, playlist, curve):
611 def _on_delete_curve(self, _class, method, playlist, curve):
612 index = playlist.index(curve)
613 results = self.execute_command(
614 command=self._command_by_name('remove curve from playlist'),
615 args={'index': index})
616 #os.remove(curve.path)
619 def _on_set_selected_playlist(self, _class, method, playlist):
620 """Call the `jump to playlist` command.
622 results = self.execute_command(
623 command=self._command_by_name('playlists'))
624 if not isinstance(results[-1], Success):
626 assert len(results) == 2, results
627 playlists = results[0]
628 matching = [p for p in playlists if p.name == playlist.name]
629 assert len(matching) == 1, matching
630 index = playlists.index(matching[0])
631 results = self.execute_command(
632 command=self._command_by_name('jump to playlist'),
633 args={'index':index})
635 def _on_set_selected_curve(self, _class, method, playlist, curve):
636 """Call the `jump to curve` command.
638 self._on_set_selected_playlist(_class, method, playlist)
639 index = playlist.index(curve)
640 results = self.execute_command(
641 command=self._command_by_name('jump to curve'),
642 args={'index':index})
643 if not isinstance(results[-1], Success):
645 #results = self.execute_command(
646 # command=self._command_by_name('get playlist'))
647 #if not isinstance(results[-1], Success):
649 self.execute_command(
650 command=self._command_by_name('get curve'))
654 # Plot panel interface
656 def _on_plot_status_text(self, _class, method, text):
657 if 'status bar' in self._c:
658 self._c['status bar'].set_plot_text(text)
664 def _next_curve(self, *args):
665 """Call the `next curve` command.
667 results = self.execute_command(
668 command=self._command_by_name('next curve'))
669 if isinstance(results[-1], Success):
670 self.execute_command(
671 command=self._command_by_name('get curve'))
673 def _previous_curve(self, *args):
674 """Call the `previous curve` command.
676 results = self.execute_command(
677 command=self._command_by_name('previous curve'))
678 if isinstance(results[-1], Success):
679 self.execute_command(
680 command=self._command_by_name('get curve'))
684 # Panel display handling
686 def _on_pane_close(self, event):
688 view = self._c['menu bar']._c['view']
689 if pane.name in view._c.keys():
690 view._c[pane.name].Check(False)
693 def _on_panel_visibility(self, _class, method, panel_name, visible):
694 pane = self._c['manager'].GetPane(panel_name)
696 #if we don't do the following, the Folders pane does not resize properly on hide/show
697 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
698 #folders_size = pane.GetSize()
699 self.panelFolders.Fit()
700 self._c['manager'].Update()
702 def _setup_perspectives(self):
703 """Add perspectives to menubar and _perspectives.
705 self._perspectives = {
706 'Default': self._c['manager'].SavePerspective(),
708 path = os.path.expanduser(self.gui.config['perspective path'])
709 if os.path.isdir(path):
710 files = sorted(os.listdir(path))
712 name, extension = os.path.splitext(fname)
713 if extension != self.gui.config['perspective extension']:
715 fpath = os.path.join(path, fname)
716 if not os.path.isfile(fpath):
719 with open(fpath, 'rU') as f:
720 perspective = f.readline()
722 self._perspectives[name] = perspective
724 selected_perspective = self.gui.config['active perspective']
725 if not self._perspectives.has_key(selected_perspective):
726 self._set_config('active perspective', 'Default')
728 self._restore_perspective(selected_perspective, force=True)
729 self._update_perspective_menu()
731 def _update_perspective_menu(self):
732 self._c['menu bar']._c['perspective'].update(
733 sorted(self._perspectives.keys()),
734 self.gui.config['active perspective'])
736 def _save_perspective(self, perspective, perspective_dir, name,
738 path = os.path.join(perspective_dir, name)
739 if extension != None:
741 if not os.path.isdir(perspective_dir):
742 os.makedirs(perspective_dir)
743 with open(path, 'w') as f:
745 self._perspectives[name] = perspective
746 self._restore_perspective(name)
747 self._update_perspective_menu()
749 def _delete_perspectives(self, perspective_dir, names,
751 self.log.debug('remove perspectives %s from %s'
752 % (names, perspective_dir))
754 path = os.path.join(perspective_dir, name)
755 if extension != None:
758 del(self._perspectives[name])
759 self._update_perspective_menu()
760 if self.gui.config['active perspective'] in names:
761 self._restore_perspective('Default')
762 # TODO: does this bug still apply?
763 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
764 # http://trac.wxwidgets.org/ticket/3258
765 # ) that makes the radio item indicator in the menu disappear.
766 # The code should be fine once this issue is fixed.
768 def _restore_perspective(self, name, force=False):
769 if name != self.gui.config['active perspective'] or force == True:
770 self.log.debug('restore perspective %s' % name)
771 self._set_config('active perspective', name)
772 self._c['manager'].LoadPerspective(self._perspectives[name])
773 self._c['manager'].Update()
774 for pane in self._c['manager'].GetAllPanes():
775 view = self._c['menu bar']._c['view']
776 if pane.name in view._c.keys():
777 view._c[pane.name].Check(pane.window.IsShown())
779 def _on_save_perspective(self, *args):
780 perspective = self._c['manager'].SavePerspective()
781 name = self.gui.config['active perspective']
782 if name == 'Default':
783 name = 'New perspective'
784 name = select_save_file(
785 directory=os.path.expanduser(self.gui.config['perspective path']),
787 extension=self.gui.config['perspective extension'],
789 message='Enter a name for the new perspective:',
790 caption='Save perspective')
793 self._save_perspective(
795 os.path.expanduser(self.gui.config['perspective path']), name=name,
796 extension=self.gui.config['perspective extension'])
798 def _on_delete_perspective(self, *args, **kwargs):
799 options = sorted([p for p in self._perspectives.keys()
801 dialog = SelectionDialog(
803 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
804 button_id=wx.ID_DELETE,
805 selection_style='multiple',
807 title='Delete perspective(s)',
808 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
809 dialog.CenterOnScreen()
811 if dialog.canceled == True:
813 names = [options[i] for i in dialog.selected]
815 self._delete_perspectives(
816 os.path.expanduser(self.gui.config['perspective path']),
817 names=names, extension=self.gui.config['perspective extension'])
819 def _on_select_perspective(self, _class, method, name):
820 self._restore_perspective(name)
823 # setup per-command versions of HookeFrame._update_curve
824 for _command in ['convert_distance_to_force',
826 'remove_cantilever_from_extension',
827 'zero_surface_contact_point',
829 setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
833 class HookeApp (wx.App):
834 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
836 Tosses up a splash screen and then loads :class:`HookeFrame` in
839 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
841 self.commands = commands
842 self.inqueue = inqueue
843 self.outqueue = outqueue
844 super(HookeApp, self).__init__(*args, **kwargs)
847 self.SetAppName('Hooke')
848 self.SetVendorName('')
849 self._setup_splash_screen()
851 height = self.gui.config['main height']
852 width = self.gui.config['main width']
853 top = self.gui.config['main top']
854 left = self.gui.config['main left']
856 # Sometimes, the ini file gets confused and sets 'left' and
857 # 'top' to large negative numbers. Here we catch and fix
858 # this. Keep small negative numbers, the user might want
867 self.gui, self.commands, self.inqueue, self.outqueue,
868 parent=None, title='Hooke',
869 pos=(left, top), size=(width, height),
870 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
872 self._c['frame'].Show(True)
873 self.SetTopWindow(self._c['frame'])
876 def _setup_splash_screen(self):
877 if self.gui.config['show splash screen'] == True:
878 path = os.path.expanduser(self.gui.config['splash screen image'])
879 if os.path.isfile(path):
880 duration = self.gui.config['splash screen duration']
882 bitmap=wx.Image(path).ConvertToBitmap(),
883 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
884 milliseconds=duration,
887 # For some reason splashDuration and sleep do not
888 # correspond to each other at least not on Windows.
889 # Maybe it's because duration is in milliseconds and
890 # sleep in seconds. Thus we need to increase the
891 # sleep time a bit. A factor of 1.2 seems to work.
893 time.sleep(sleepFactor * duration / 1000)
896 class GUI (UserInterface):
897 """wxWindows graphical user interface.
900 super(GUI, self).__init__(name='gui')
902 def default_settings(self):
903 """Return a list of :class:`hooke.config.Setting`\s for any
904 configurable UI settings.
906 The suggested section setting is::
908 Setting(section=self.setting_section, help=self.__doc__)
911 Setting(section=self.setting_section, help=self.__doc__),
912 Setting(section=self.setting_section, option='icon image',
913 value=os.path.join('doc', 'img', 'microscope.ico'),
915 help='Path to the hooke icon image.'),
916 Setting(section=self.setting_section, option='show splash screen',
917 value=True, type='bool',
918 help='Enable/disable the splash screen'),
919 Setting(section=self.setting_section, option='splash screen image',
920 value=os.path.join('doc', 'img', 'hooke.jpg'),
922 help='Path to the Hooke splash screen image.'),
923 Setting(section=self.setting_section,
924 option='splash screen duration',
925 value=1000, type='int',
926 help='Duration of the splash screen in milliseconds.'),
927 Setting(section=self.setting_section, option='perspective path',
928 value=os.path.join('resources', 'gui', 'perspective'),
929 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
930 Setting(section=self.setting_section, option='perspective extension',
932 help='Extension for perspective files.'),
933 Setting(section=self.setting_section, option='hide extensions',
934 value=False, type='bool',
935 help='Hide file extensions when displaying names.'),
936 Setting(section=self.setting_section, option='plot legend',
937 value=True, type='bool',
938 help='Enable/disable the plot legend.'),
939 Setting(section=self.setting_section, option='plot SI format',
940 value='True', type='bool',
941 help='Enable/disable SI plot axes numbering.'),
942 Setting(section=self.setting_section, option='plot decimals',
944 help='Number of decimal places to show if "plot SI format" is enabled.'),
945 Setting(section=self.setting_section, option='folders-workdir',
946 value='.', type='path',
947 help='This should probably go...'),
948 Setting(section=self.setting_section, option='folders-filters',
949 value='.', type='path',
950 help='This should probably go...'),
951 Setting(section=self.setting_section, option='active perspective',
953 help='Name of active perspective file (or "Default").'),
954 Setting(section=self.setting_section,
955 option='folders-filter-index',
957 help='This should probably go...'),
958 Setting(section=self.setting_section, option='main height',
959 value=450, type='int',
960 help='Height of main window in pixels.'),
961 Setting(section=self.setting_section, option='main width',
962 value=800, type='int',
963 help='Width of main window in pixels.'),
964 Setting(section=self.setting_section, option='main top',
966 help='Pixels from screen top to top of main window.'),
967 Setting(section=self.setting_section, option='main left',
969 help='Pixels from screen left to left of main window.'),
970 Setting(section=self.setting_section, option='selected command',
971 value='load playlist',
972 help='Name of the initially selected command.'),
975 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
979 app = HookeApp(gui=self,
981 inqueue=ui_to_command_queue,
982 outqueue=command_to_ui_queue,
986 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
987 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)