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 _update_curve(self, command, args={}, results=[]):
523 """Update the curve, since the available columns may have changed.
525 if isinstance(results[-1], Success):
526 self.execute_command(
527 command=self._command_by_name('get curve'))
530 # Command panel interface
532 def select_command(self, _class, method, command):
533 #self.select_plugin(plugin=command.plugin)
534 self._c['property editor'].clear()
535 self._c['property editor']._argument_from_label = {}
536 for argument in command.arguments:
537 if argument.name == 'help':
540 results = self.execute_command(
541 command=self._command_by_name('playlists'))
542 if not isinstance(results[-1], Success):
543 self._postprocess_text(command, results=results)
546 playlists = results[0]
548 results = self.execute_command(
549 command=self._command_by_name('playlist curves'))
550 if not isinstance(results[-1], Success):
551 self._postprocess_text(command, results=results)
556 ret = props_from_argument(
557 argument, curves=curves, playlists=playlists)
559 continue # property intentionally not handled (yet)
561 self._c['property editor'].append_property(p)
562 self._c['property editor']._argument_from_label[label] = (
565 self._set_config('selected command', command.name)
567 def select_plugin(self, _class=None, method=None, plugin=None):
572 # Folders panel interface
574 def _on_dir_ctrl_left_double_click(self, event):
575 file_path = self.panelFolders.GetPath()
576 if os.path.isfile(file_path):
577 if file_path.endswith('.hkp'):
578 self.do_loadlist(file_path)
583 # Note panel interface
585 def _on_update_note(self, _class, method, text):
586 """Sets the note for the active curve.
588 self.execute_command(
589 command=self._command_by_name('set note'),
594 # Playlist panel interface
596 def _on_user_delete_playlist(self, _class, method, playlist):
599 def _on_delete_playlist(self, _class, method, playlist):
600 if hasattr(playlist, 'path') and playlist.path != None:
601 os.remove(playlist.path)
603 def _on_user_delete_curve(self, _class, method, playlist, curve):
606 def _on_delete_curve(self, _class, method, playlist, curve):
607 # TODO: execute_command 'remove curve from playlist'
608 #os.remove(curve.path)
611 def _on_set_selected_playlist(self, _class, method, playlist):
612 """Call the `jump to playlist` command.
614 results = self.execute_command(
615 command=self._command_by_name('playlists'))
616 if not isinstance(results[-1], Success):
618 assert len(results) == 2, results
619 playlists = results[0]
620 matching = [p for p in playlists if p.name == playlist.name]
621 assert len(matching) == 1, matching
622 index = playlists.index(matching[0])
623 results = self.execute_command(
624 command=self._command_by_name('jump to playlist'),
625 args={'index':index})
627 def _on_set_selected_curve(self, _class, method, playlist, curve):
628 """Call the `jump to curve` command.
630 self._on_set_selected_playlist(_class, method, playlist)
631 index = playlist.index(curve)
632 results = self.execute_command(
633 command=self._command_by_name('jump to curve'),
634 args={'index':index})
635 if not isinstance(results[-1], Success):
637 #results = self.execute_command(
638 # command=self._command_by_name('get playlist'))
639 #if not isinstance(results[-1], Success):
641 self.execute_command(
642 command=self._command_by_name('get curve'))
646 # Plot panel interface
648 def _on_plot_status_text(self, _class, method, text):
649 if 'status bar' in self._c:
650 self._c['status bar'].set_plot_text(text)
656 def _next_curve(self, *args):
657 """Call the `next curve` command.
659 results = self.execute_command(
660 command=self._command_by_name('next curve'))
661 if isinstance(results[-1], Success):
662 self.execute_command(
663 command=self._command_by_name('get curve'))
665 def _previous_curve(self, *args):
666 """Call the `previous curve` command.
668 results = self.execute_command(
669 command=self._command_by_name('previous curve'))
670 if isinstance(results[-1], Success):
671 self.execute_command(
672 command=self._command_by_name('get curve'))
676 # Panel display handling
678 def _on_pane_close(self, event):
680 view = self._c['menu bar']._c['view']
681 if pane.name in view._c.keys():
682 view._c[pane.name].Check(False)
685 def _on_panel_visibility(self, _class, method, panel_name, visible):
686 pane = self._c['manager'].GetPane(panel_name)
688 #if we don't do the following, the Folders pane does not resize properly on hide/show
689 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
690 #folders_size = pane.GetSize()
691 self.panelFolders.Fit()
692 self._c['manager'].Update()
694 def _setup_perspectives(self):
695 """Add perspectives to menubar and _perspectives.
697 self._perspectives = {
698 'Default': self._c['manager'].SavePerspective(),
700 path = os.path.expanduser(self.gui.config['perspective path'])
701 if os.path.isdir(path):
702 files = sorted(os.listdir(path))
704 name, extension = os.path.splitext(fname)
705 if extension != self.gui.config['perspective extension']:
707 fpath = os.path.join(path, fname)
708 if not os.path.isfile(fpath):
711 with open(fpath, 'rU') as f:
712 perspective = f.readline()
714 self._perspectives[name] = perspective
716 selected_perspective = self.gui.config['active perspective']
717 if not self._perspectives.has_key(selected_perspective):
718 self._set_config('active perspective', 'Default')
720 self._restore_perspective(selected_perspective, force=True)
721 self._update_perspective_menu()
723 def _update_perspective_menu(self):
724 self._c['menu bar']._c['perspective'].update(
725 sorted(self._perspectives.keys()),
726 self.gui.config['active perspective'])
728 def _save_perspective(self, perspective, perspective_dir, name,
730 path = os.path.join(perspective_dir, name)
731 if extension != None:
733 if not os.path.isdir(perspective_dir):
734 os.makedirs(perspective_dir)
735 with open(path, 'w') as f:
737 self._perspectives[name] = perspective
738 self._restore_perspective(name)
739 self._update_perspective_menu()
741 def _delete_perspectives(self, perspective_dir, names,
743 self.log.debug('remove perspectives %s from %s'
744 % (names, perspective_dir))
746 path = os.path.join(perspective_dir, name)
747 if extension != None:
750 del(self._perspectives[name])
751 self._update_perspective_menu()
752 if self.gui.config['active perspective'] in names:
753 self._restore_perspective('Default')
754 # TODO: does this bug still apply?
755 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
756 # http://trac.wxwidgets.org/ticket/3258
757 # ) that makes the radio item indicator in the menu disappear.
758 # The code should be fine once this issue is fixed.
760 def _restore_perspective(self, name, force=False):
761 if name != self.gui.config['active perspective'] or force == True:
762 self.log.debug('restore perspective %s' % name)
763 self._set_config('active perspective', name)
764 self._c['manager'].LoadPerspective(self._perspectives[name])
765 self._c['manager'].Update()
766 for pane in self._c['manager'].GetAllPanes():
767 view = self._c['menu bar']._c['view']
768 if pane.name in view._c.keys():
769 view._c[pane.name].Check(pane.window.IsShown())
771 def _on_save_perspective(self, *args):
772 perspective = self._c['manager'].SavePerspective()
773 name = self.gui.config['active perspective']
774 if name == 'Default':
775 name = 'New perspective'
776 name = select_save_file(
777 directory=os.path.expanduser(self.gui.config['perspective path']),
779 extension=self.gui.config['perspective extension'],
781 message='Enter a name for the new perspective:',
782 caption='Save perspective')
785 self._save_perspective(
787 os.path.expanduser(self.gui.config['perspective path']), name=name,
788 extension=self.gui.config['perspective extension'])
790 def _on_delete_perspective(self, *args, **kwargs):
791 options = sorted([p for p in self._perspectives.keys()
793 dialog = SelectionDialog(
795 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
796 button_id=wx.ID_DELETE,
797 selection_style='multiple',
799 title='Delete perspective(s)',
800 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
801 dialog.CenterOnScreen()
803 if dialog.canceled == True:
805 names = [options[i] for i in dialog.selected]
807 self._delete_perspectives(
808 os.path.expanduser(self.gui.config['perspective path']),
809 names=names, extension=self.gui.config['perspective extension'])
811 def _on_select_perspective(self, _class, method, name):
812 self._restore_perspective(name)
815 # setup per-command versions of HookeFrame._update_curve
816 for _command in ['convert_distance_to_force',
818 'remove_cantilever_from_extension',
819 'zero_surface_contact_point',
821 setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
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)