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)
610 def _on_set_selected_playlist(self, _class, method, playlist):
611 """Call the `jump to playlist` command.
613 results = self.execute_command(
614 command=self._command_by_name('playlists'))
615 if not isinstance(results[-1], Success):
617 assert len(results) == 2, results
618 playlists = results[0]
619 matching = [p for p in playlists if p.name == playlist.name]
620 assert len(matching) == 1, matching
621 index = playlists.index(matching[0])
622 results = self.execute_command(
623 command=self._command_by_name('jump to playlist'),
624 args={'index':index})
626 def _on_set_selected_curve(self, _class, method, playlist, curve):
627 """Call the `jump to curve` command.
629 self._on_set_selected_playlist(_class, method, playlist)
630 index = playlist.index(curve)
631 results = self.execute_command(
632 command=self._command_by_name('jump to curve'),
633 args={'index':index})
634 if not isinstance(results[-1], Success):
636 #results = self.execute_command(
637 # command=self._command_by_name('get playlist'))
638 #if not isinstance(results[-1], Success):
640 self.execute_command(
641 command=self._command_by_name('get curve'))
645 # Plot panel interface
647 def _on_plot_status_text(self, _class, method, text):
648 if 'status bar' in self._c:
649 self._c['status bar'].set_plot_text(text)
655 def _next_curve(self, *args):
656 """Call the `next curve` command.
658 results = self.execute_command(
659 command=self._command_by_name('next curve'))
660 if isinstance(results[-1], Success):
661 self.execute_command(
662 command=self._command_by_name('get curve'))
664 def _previous_curve(self, *args):
665 """Call the `previous curve` command.
667 results = self.execute_command(
668 command=self._command_by_name('previous curve'))
669 if isinstance(results[-1], Success):
670 self.execute_command(
671 command=self._command_by_name('get curve'))
675 # Panel display handling
677 def _on_pane_close(self, event):
679 view = self._c['menu bar']._c['view']
680 if pane.name in view._c.keys():
681 view._c[pane.name].Check(False)
684 def _on_panel_visibility(self, _class, method, panel_name, visible):
685 pane = self._c['manager'].GetPane(panel_name)
687 #if we don't do the following, the Folders pane does not resize properly on hide/show
688 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
689 #folders_size = pane.GetSize()
690 self.panelFolders.Fit()
691 self._c['manager'].Update()
693 def _setup_perspectives(self):
694 """Add perspectives to menubar and _perspectives.
696 self._perspectives = {
697 'Default': self._c['manager'].SavePerspective(),
699 path = os.path.expanduser(self.gui.config['perspective path'])
700 if os.path.isdir(path):
701 files = sorted(os.listdir(path))
703 name, extension = os.path.splitext(fname)
704 if extension != self.gui.config['perspective extension']:
706 fpath = os.path.join(path, fname)
707 if not os.path.isfile(fpath):
710 with open(fpath, 'rU') as f:
711 perspective = f.readline()
713 self._perspectives[name] = perspective
715 selected_perspective = self.gui.config['active perspective']
716 if not self._perspectives.has_key(selected_perspective):
717 self._set_config('active perspective', 'Default')
719 self._restore_perspective(selected_perspective, force=True)
720 self._update_perspective_menu()
722 def _update_perspective_menu(self):
723 self._c['menu bar']._c['perspective'].update(
724 sorted(self._perspectives.keys()),
725 self.gui.config['active perspective'])
727 def _save_perspective(self, perspective, perspective_dir, name,
729 path = os.path.join(perspective_dir, name)
730 if extension != None:
732 if not os.path.isdir(perspective_dir):
733 os.makedirs(perspective_dir)
734 with open(path, 'w') as f:
736 self._perspectives[name] = perspective
737 self._restore_perspective(name)
738 self._update_perspective_menu()
740 def _delete_perspectives(self, perspective_dir, names,
742 self.log.debug('remove perspectives %s from %s'
743 % (names, perspective_dir))
745 path = os.path.join(perspective_dir, name)
746 if extension != None:
749 del(self._perspectives[name])
750 self._update_perspective_menu()
751 if self.gui.config['active perspective'] in names:
752 self._restore_perspective('Default')
753 # TODO: does this bug still apply?
754 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
755 # http://trac.wxwidgets.org/ticket/3258
756 # ) that makes the radio item indicator in the menu disappear.
757 # The code should be fine once this issue is fixed.
759 def _restore_perspective(self, name, force=False):
760 if name != self.gui.config['active perspective'] or force == True:
761 self.log.debug('restore perspective %s' % name)
762 self._set_config('active perspective', name)
763 self._c['manager'].LoadPerspective(self._perspectives[name])
764 self._c['manager'].Update()
765 for pane in self._c['manager'].GetAllPanes():
766 view = self._c['menu bar']._c['view']
767 if pane.name in view._c.keys():
768 view._c[pane.name].Check(pane.window.IsShown())
770 def _on_save_perspective(self, *args):
771 perspective = self._c['manager'].SavePerspective()
772 name = self.gui.config['active perspective']
773 if name == 'Default':
774 name = 'New perspective'
775 name = select_save_file(
776 directory=os.path.expanduser(self.gui.config['perspective path']),
778 extension=self.gui.config['perspective extension'],
780 message='Enter a name for the new perspective:',
781 caption='Save perspective')
784 self._save_perspective(
786 os.path.expanduser(self.gui.config['perspective path']), name=name,
787 extension=self.gui.config['perspective extension'])
789 def _on_delete_perspective(self, *args, **kwargs):
790 options = sorted([p for p in self._perspectives.keys()
792 dialog = SelectionDialog(
794 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
795 button_id=wx.ID_DELETE,
796 selection_style='multiple',
798 title='Delete perspective(s)',
799 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
800 dialog.CenterOnScreen()
802 if dialog.canceled == True:
804 names = [options[i] for i in dialog.selected]
806 self._delete_perspectives(
807 os.path.expanduser(self.gui.config['perspective path']),
808 names=names, extension=self.gui.config['perspective extension'])
810 def _on_select_perspective(self, _class, method, name):
811 self._restore_perspective(name)
814 # setup per-command versions of HookeFrame._update_curve
815 for _command in ['convert_distance_to_force',
817 'remove_cantilever_from_extension',
818 'zero_surface_contact_point',
820 setattr(HookeFrame, '_postprocess_%s' % _command, HookeFrame._update_curve)
824 class HookeApp (wx.App):
825 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
827 Tosses up a splash screen and then loads :class:`HookeFrame` in
830 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
832 self.commands = commands
833 self.inqueue = inqueue
834 self.outqueue = outqueue
835 super(HookeApp, self).__init__(*args, **kwargs)
838 self.SetAppName('Hooke')
839 self.SetVendorName('')
840 self._setup_splash_screen()
842 height = self.gui.config['main height']
843 width = self.gui.config['main width']
844 top = self.gui.config['main top']
845 left = self.gui.config['main left']
847 # Sometimes, the ini file gets confused and sets 'left' and
848 # 'top' to large negative numbers. Here we catch and fix
849 # this. Keep small negative numbers, the user might want
858 self.gui, self.commands, self.inqueue, self.outqueue,
859 parent=None, title='Hooke',
860 pos=(left, top), size=(width, height),
861 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
863 self._c['frame'].Show(True)
864 self.SetTopWindow(self._c['frame'])
867 def _setup_splash_screen(self):
868 if self.gui.config['show splash screen'] == True:
869 path = os.path.expanduser(self.gui.config['splash screen image'])
870 if os.path.isfile(path):
871 duration = self.gui.config['splash screen duration']
873 bitmap=wx.Image(path).ConvertToBitmap(),
874 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
875 milliseconds=duration,
878 # For some reason splashDuration and sleep do not
879 # correspond to each other at least not on Windows.
880 # Maybe it's because duration is in milliseconds and
881 # sleep in seconds. Thus we need to increase the
882 # sleep time a bit. A factor of 1.2 seems to work.
884 time.sleep(sleepFactor * duration / 1000)
887 class GUI (UserInterface):
888 """wxWindows graphical user interface.
891 super(GUI, self).__init__(name='gui')
893 def default_settings(self):
894 """Return a list of :class:`hooke.config.Setting`\s for any
895 configurable UI settings.
897 The suggested section setting is::
899 Setting(section=self.setting_section, help=self.__doc__)
902 Setting(section=self.setting_section, help=self.__doc__),
903 Setting(section=self.setting_section, option='icon image',
904 value=os.path.join('doc', 'img', 'microscope.ico'),
906 help='Path to the hooke icon image.'),
907 Setting(section=self.setting_section, option='show splash screen',
908 value=True, type='bool',
909 help='Enable/disable the splash screen'),
910 Setting(section=self.setting_section, option='splash screen image',
911 value=os.path.join('doc', 'img', 'hooke.jpg'),
913 help='Path to the Hooke splash screen image.'),
914 Setting(section=self.setting_section,
915 option='splash screen duration',
916 value=1000, type='int',
917 help='Duration of the splash screen in milliseconds.'),
918 Setting(section=self.setting_section, option='perspective path',
919 value=os.path.join('resources', 'gui', 'perspective'),
920 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
921 Setting(section=self.setting_section, option='perspective extension',
923 help='Extension for perspective files.'),
924 Setting(section=self.setting_section, option='hide extensions',
925 value=False, type='bool',
926 help='Hide file extensions when displaying names.'),
927 Setting(section=self.setting_section, option='plot legend',
928 value=True, type='bool',
929 help='Enable/disable the plot legend.'),
930 Setting(section=self.setting_section, option='plot SI format',
931 value='True', type='bool',
932 help='Enable/disable SI plot axes numbering.'),
933 Setting(section=self.setting_section, option='plot decimals',
935 help='Number of decimal places to show if "plot SI format" is enabled.'),
936 Setting(section=self.setting_section, option='folders-workdir',
937 value='.', type='path',
938 help='This should probably go...'),
939 Setting(section=self.setting_section, option='folders-filters',
940 value='.', type='path',
941 help='This should probably go...'),
942 Setting(section=self.setting_section, option='active perspective',
944 help='Name of active perspective file (or "Default").'),
945 Setting(section=self.setting_section,
946 option='folders-filter-index',
948 help='This should probably go...'),
949 Setting(section=self.setting_section, option='main height',
950 value=450, type='int',
951 help='Height of main window in pixels.'),
952 Setting(section=self.setting_section, option='main width',
953 value=800, type='int',
954 help='Width of main window in pixels.'),
955 Setting(section=self.setting_section, option='main top',
957 help='Pixels from screen top to top of main window.'),
958 Setting(section=self.setting_section, option='main left',
960 help='Pixels from screen left to left of main window.'),
961 Setting(section=self.setting_section, option='selected command',
962 value='load playlist',
963 help='Name of the initially selected command.'),
966 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
970 app = HookeApp(gui=self,
972 inqueue=ui_to_command_queue,
973 outqueue=command_to_ui_queue,
977 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
978 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)