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.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.OnPaneClose)
247 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
249 return # TODO: cleanup
250 treeCtrl = self._c['folders'].GetTreeCtrl()
251 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
254 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
256 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
258 def _on_about(self, *args):
259 dialog = wx.MessageDialog(
261 message=self.gui._splash_text(extra_info={
262 'get-details':'click "Help -> License"'},
264 caption='About Hooke',
265 style=wx.OK|wx.ICON_INFORMATION)
269 def _on_close(self, *args):
270 self.log.info('closing GUI framework')
272 self.gui.config['main height'] = str(self.GetSize().GetHeight())
273 self.gui.config['main left'] = str(self.GetPosition()[0])
274 self.gui.config['main top'] = str(self.GetPosition()[1])
275 self.gui.config['main width'] = str(self.GetSize().GetWidth())
276 # push changes back to Hooke.config?
277 self._c['manager'].UnInit()
278 del self._c['manager']
283 # Panel utility functions
285 def _file_name(self, name):
286 """Cleanup names according to configured preferences.
288 if self.gui.config['hide extensions'] == True:
289 name,ext = os.path.splitext(name)
296 def _command_by_name(self, name):
297 cs = [c for c in self.commands if c.name == name]
301 raise Exception('Multiple commands named "%s"' % name)
304 def execute_command(self, _class=None, method=None,
305 command=None, args=None):
308 if ('property editor' in self._c
309 and self.gui.config['selected command'] == command):
310 for name,value in self._c['property editor'].get_values().items():
311 arg = self._c['property editor']._argument_from_label.get(
316 args[arg.name] = value
318 # deal with counted arguments
319 if arg.name not in args:
321 index = int(name[len(arg.name):])
322 args[arg.name][index] = value
323 for arg in command.arguments:
324 if arg.name not in args:
325 continue # undisplayed argument, e.g. 'driver' types.
327 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
328 count = arg._display_count
329 if count != 1 and arg.name in args:
330 keys = sorted(args[arg.name].keys())
331 assert keys == range(count), keys
332 args[arg.name] = [args[arg.name][i]
333 for i in range(count)]
335 while (len(args[arg.name]) > 0
336 and args[arg.name][-1] == None):
338 if len(args[arg.name]) == 0:
339 args[arg.name] = arg.default
340 cm = CommandMessage(command.name, args)
341 self.gui._submit_command(cm, self.inqueue)
344 msg = self.outqueue.get()
346 if isinstance(msg, Exit):
349 elif isinstance(msg, CommandExit):
350 # TODO: display command complete
352 elif isinstance(msg, ReloadUserInterfaceConfig):
353 self.gui.reload_config(msg.config)
355 elif isinstance(msg, Request):
356 h = handler.HANDLERS[msg.type]
357 h.run(self, msg) # TODO: pause for response?
360 self, '_postprocess_%s' % command.name.replace(' ', '_'),
361 self._postprocess_text)
362 pp(command=command, args=args, results=results)
365 def _handle_request(self, msg):
366 """Repeatedly try to get a response to `msg`.
369 raise NotImplementedError('_%s_request_prompt' % msg.type)
370 prompt_string = prompt(msg)
371 parser = getattr(self, '_%s_request_parser' % msg.type, None)
373 raise NotImplementedError('_%s_request_parser' % msg.type)
377 self.cmd.stdout.write(''.join([
378 error.__class__.__name__, ': ', str(error), '\n']))
379 self.cmd.stdout.write(prompt_string)
380 value = parser(msg, self.cmd.stdin.readline())
382 response = msg.response(value)
384 except ValueError, error:
386 self.inqueue.put(response)
390 # Command-specific postprocessing
392 def _postprocess_text(self, command, args={}, results=[]):
393 """Print the string representation of the results to the Results window.
395 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
396 approach, except that :class:`~hooke.ui.commandline.DoCommand`
397 doesn't print some internally handled messages
398 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
400 for result in results:
401 if isinstance(result, CommandExit):
402 self._c['output'].write(result.__class__.__name__+'\n')
403 self._c['output'].write(str(result).rstrip()+'\n')
405 def _postprocess_playlists(self, command, args={}, results=None):
406 """Update `self` to show the playlists.
408 if not isinstance(results[-1], Success):
409 self._postprocess_text(command, results=results)
411 assert len(results) == 2, results
412 playlists = results[0]
413 if 'playlist' in self._c:
414 for playlist in playlists:
415 if self._c['playlist'].is_playlist_loaded(playlist):
416 self._c['playlist'].update_playlist(playlist)
418 self._c['playlist'].add_playlist(playlist)
420 def _postprocess_new_playlist(self, command, args={}, results=None):
421 """Update `self` to show the new playlist.
423 if not isinstance(results[-1], Success):
424 self._postprocess_text(command, results=results)
426 assert len(results) == 2, results
427 playlist = results[0]
428 if 'playlist' in self._c:
429 loaded = self._c['playlist'].is_playlist_loaded(playlist)
430 assert loaded == False, loaded
431 self._c['playlist'].add_playlist(playlist)
433 def _postprocess_load_playlist(self, command, args={}, results=None):
434 """Update `self` to show the playlist.
436 if not isinstance(results[-1], Success):
437 self._postprocess_text(command, results=results)
439 assert len(results) == 2, results
440 playlist = results[0]
441 self._c['playlist'].add_playlist(playlist)
443 def _postprocess_get_playlist(self, command, args={}, results=[]):
444 if not isinstance(results[-1], Success):
445 self._postprocess_text(command, results=results)
447 assert len(results) == 2, results
448 playlist = results[0]
449 if 'playlist' in self._c:
450 loaded = self._c['playlist'].is_playlist_loaded(playlist)
451 assert loaded == True, loaded
452 self._c['playlist'].update_playlist(playlist)
454 def _postprocess_get_curve(self, command, args={}, results=[]):
455 """Update `self` to show the curve.
457 if not isinstance(results[-1], Success):
458 self._postprocess_text(command, results=results)
460 assert len(results) == 2, results
462 if args.get('curve', None) == None:
463 # the command defaults to the current curve of the current playlist
464 results = self.execute_command(
465 command=self._command_by_name('get playlist'))
466 playlist = results[0]
468 raise NotImplementedError()
469 if 'note' in self._c:
470 self._c['note'].set_text(curve.info.get('note', ''))
471 if 'playlist' in self._c:
472 self._c['playlist'].set_selected_curve(
474 if 'plot' in self._c:
475 self._c['plot'].set_curve(curve, config=self.gui.config)
477 def _postprocess_next_curve(self, command, args={}, results=[]):
478 """No-op. Only call 'next curve' via `self._next_curve()`.
482 def _postprocess_previous_curve(self, command, args={}, results=[]):
483 """No-op. Only call 'previous curve' via `self._previous_curve()`.
487 def _postprocess_glob_curves_to_playlist(
488 self, command, args={}, results=[]):
489 """Update `self` to show new curves.
491 if not isinstance(results[-1], Success):
492 self._postprocess_text(command, results=results)
494 if 'playlist' in self._c:
495 if args.get('playlist', None) != None:
496 playlist = args['playlist']
497 pname = playlist.name
498 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
499 assert loaded == True, loaded
500 for curve in results[:-1]:
501 self._c['playlist']._add_curve(pname, curve)
503 self.execute_command(
504 command=self._command_by_name('get playlist'))
506 def _postprocess_zero_block_surface_contact_point(
507 self, command, args={}, results=[]):
508 """Update the curve, since the available columns may have changed.
510 if isinstance(results[-1], Success):
511 self.execute_command(
512 command=self._command_by_name('get curve'))
514 def _postprocess_add_block_force_array(
515 self, command, args={}, results=[]):
516 """Update the curve, since the available columns may have changed.
518 if isinstance(results[-1], Success):
519 self.execute_command(
520 command=self._command_by_name('get curve'))
526 def _GetActiveFileIndex(self):
527 lib.playlist.Playlist = self.GetActivePlaylist()
528 #get the selected item from the tree
529 selected_item = self._c['playlist']._c['tree'].GetSelection()
530 #test if a playlist or a curve was double-clicked
531 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
535 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
536 while selected_item.IsOk():
538 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
541 def _GetPlaylistTab(self, name):
542 for index, page in enumerate(self._c['notebook']._tabs._pages):
543 if page.caption == name:
547 def select_plugin(self, _class=None, method=None, plugin=None):
550 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
552 playlist = lib.playlist.Playlist(self, self.drivers)
554 playlist.add_curve(item)
555 if playlist.count > 0:
556 playlist.name = self._GetUniquePlaylistName(name)
558 self.AddTayliss(playlist)
560 def AppliesPlotmanipulator(self, name):
562 Returns True if the plotmanipulator 'name' is applied, False otherwise
563 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
565 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
567 def ApplyPlotmanipulators(self, plot, plot_file):
569 Apply all active plotmanipulators.
571 if plot is not None and plot_file is not None:
572 manipulated_plot = copy.deepcopy(plot)
573 for plotmanipulator in self.plotmanipulators:
574 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
575 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
576 return manipulated_plot
578 def GetActiveFigure(self):
579 playlist_name = self.GetActivePlaylistName()
580 figure = self.playlists[playlist_name].figure
581 if figure is not None:
585 def GetActiveFile(self):
586 playlist = self.GetActivePlaylist()
587 if playlist is not None:
588 return playlist.get_active_file()
591 def GetActivePlot(self):
592 playlist = self.GetActivePlaylist()
593 if playlist is not None:
594 return playlist.get_active_file().plot
597 def GetDisplayedPlot(self):
598 plot = copy.deepcopy(self.displayed_plot)
600 #plot.curves = copy.deepcopy(plot.curves)
603 def GetDisplayedPlotCorrected(self):
604 plot = copy.deepcopy(self.displayed_plot)
606 plot.curves = copy.deepcopy(plot.corrected_curves)
609 def GetDisplayedPlotRaw(self):
610 plot = copy.deepcopy(self.displayed_plot)
612 plot.curves = copy.deepcopy(plot.raw_curves)
615 def GetDockArt(self):
616 return self._c['manager'].GetArtProvider()
618 def GetPlotmanipulator(self, name):
620 Returns a plot manipulator function from its name
622 for plotmanipulator in self.plotmanipulators:
623 if plotmanipulator.name == name:
624 return plotmanipulator
627 def HasPlotmanipulator(self, name):
629 returns True if the plotmanipulator 'name' is loaded, False otherwise
631 for plotmanipulator in self.plotmanipulators:
632 if plotmanipulator.command == name:
637 def _on_dir_ctrl_left_double_click(self, event):
638 file_path = self.panelFolders.GetPath()
639 if os.path.isfile(file_path):
640 if file_path.endswith('.hkp'):
641 self.do_loadlist(file_path)
644 def _on_erase_background(self, event):
647 def _on_notebook_page_close(self, event):
648 ctrl = event.GetEventObject()
649 playlist_name = ctrl.GetPageText(ctrl._curpage)
650 self.DeleteFromPlaylists(playlist_name)
652 def OnPaneClose(self, event):
655 def OnPropGridChanged (self, event):
656 prop = event.GetProperty()
658 item_section = self.panelProperties.SelectedTreeItem
659 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
660 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
661 config = self.gui.config[plugin]
662 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
663 property_key = prop.GetName()
664 property_value = prop.GetDisplayedString()
666 config[property_section][property_key]['value'] = property_value
668 def OnResultsCheck(self, index, flag):
669 results = self.GetActivePlot().results
670 if results.has_key(self.results_str):
671 results[self.results_str].results[index].visible = flag
672 results[self.results_str].update()
676 def _on_size(self, event):
679 def UpdatePlaylistsTreeSelection(self):
680 playlist = self.GetActivePlaylist()
681 if playlist is not None:
682 if playlist.index >= 0:
683 self._c['status bar'].set_playlist(playlist)
687 def _on_curve_select(self, playlist, curve):
688 #create the plot tab and add playlist to the dictionary
689 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
690 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
691 #tab_index = self._c['notebook'].GetSelection()
692 playlist.figure = plotPanel.get_figure()
693 self.playlists[playlist.name] = playlist
694 #self.playlists[playlist.name] = [playlist, figure]
695 self._c['status bar'].set_playlist(playlist)
700 def _on_playlist_left_doubleclick(self):
701 index = self._c['notebook'].GetSelection()
702 current_playlist = self._c['notebook'].GetPageText(index)
703 if current_playlist != playlist_name:
704 index = self._GetPlaylistTab(playlist_name)
705 self._c['notebook'].SetSelection(index)
706 self._c['status bar'].set_playlist(playlist)
710 def _on_playlist_delete(self, playlist):
711 notebook = self.Parent.plotNotebook
712 index = self.Parent._GetPlaylistTab(playlist.name)
713 notebook.SetSelection(index)
714 notebook.DeletePage(notebook.GetSelection())
715 self.Parent.DeleteFromPlaylists(playlist_name)
719 # Command panel interface
721 def select_command(self, _class, method, command):
722 #self.select_plugin(plugin=command.plugin)
723 self._c['property editor'].clear()
724 self._c['property editor']._argument_from_label = {}
725 for argument in command.arguments:
726 if argument.name == 'help':
729 results = self.execute_command(
730 command=self._command_by_name('playlists'))
731 if not isinstance(results[-1], Success):
732 self._postprocess_text(command, results=results)
735 playlists = results[0]
737 results = self.execute_command(
738 command=self._command_by_name('playlist curves'))
739 if not isinstance(results[-1], Success):
740 self._postprocess_text(command, results=results)
745 ret = props_from_argument(
746 argument, curves=curves, playlists=playlists)
748 continue # property intentionally not handled (yet)
750 self._c['property editor'].append_property(p)
751 self._c['property editor']._argument_from_label[label] = (
754 self.gui.config['selected command'] = command # TODO: push to engine
758 # Note panel interface
760 def _on_update_note(self, _class, method, text):
761 """Sets the note for the active curve.
763 self.execute_command(
764 command=self._command_by_name('set note'),
769 # Playlist panel interface
771 def _on_user_delete_playlist(self, _class, method, playlist):
774 def _on_delete_playlist(self, _class, method, playlist):
775 if hasattr(playlist, 'path') and playlist.path != None:
776 os.remove(playlist.path)
778 def _on_user_delete_curve(self, _class, method, playlist, curve):
781 def _on_delete_curve(self, _class, method, playlist, curve):
782 # TODO: execute_command 'remove curve from playlist'
783 os.remove(curve.path)
785 def _on_set_selected_playlist(self, _class, method, playlist):
786 """Call the `jump to playlist` command.
788 results = self.execute_command(
789 command=self._command_by_name('playlists'))
790 if not isinstance(results[-1], Success):
792 assert len(results) == 2, results
793 playlists = results[0]
794 matching = [p for p in playlists if p.name == playlist.name]
795 assert len(matching) == 1, matching
796 index = playlists.index(matching[0])
797 results = self.execute_command(
798 command=self._command_by_name('jump to playlist'),
799 args={'index':index})
801 def _on_set_selected_curve(self, _class, method, playlist, curve):
802 """Call the `jump to curve` command.
804 self._on_set_selected_playlist(_class, method, playlist)
805 index = playlist.index(curve)
806 results = self.execute_command(
807 command=self._command_by_name('jump to curve'),
808 args={'index':index})
809 if not isinstance(results[-1], Success):
811 #results = self.execute_command(
812 # command=self._command_by_name('get playlist'))
813 #if not isinstance(results[-1], Success):
815 self.execute_command(
816 command=self._command_by_name('get curve'))
820 # Plot panel interface
822 def _on_plot_status_text(self, _class, method, text):
823 if 'status bar' in self._c:
824 self._c['status bar'].set_plot_text(text)
830 def _next_curve(self, *args):
831 """Call the `next curve` command.
833 results = self.execute_command(
834 command=self._command_by_name('next curve'))
835 if isinstance(results[-1], Success):
836 self.execute_command(
837 command=self._command_by_name('get curve'))
839 def _previous_curve(self, *args):
840 """Call the `previous curve` command.
842 results = self.execute_command(
843 command=self._command_by_name('previous curve'))
844 if isinstance(results[-1], Success):
845 self.execute_command(
846 command=self._command_by_name('get curve'))
850 # Panel display handling
852 def _on_panel_visibility(self, _class, method, panel_name, visible):
853 pane = self._c['manager'].GetPane(panel_name)
855 #if we don't do the following, the Folders pane does not resize properly on hide/show
856 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
857 #folders_size = pane.GetSize()
858 self.panelFolders.Fit()
859 self._c['manager'].Update()
861 def _setup_perspectives(self):
862 """Add perspectives to menubar and _perspectives.
864 self._perspectives = {
865 'Default': self._c['manager'].SavePerspective(),
867 path = self.gui.config['perspective path']
868 if os.path.isdir(path):
869 files = sorted(os.listdir(path))
871 name, extension = os.path.splitext(fname)
872 if extension != self.gui.config['perspective extension']:
874 fpath = os.path.join(path, fname)
875 if not os.path.isfile(fpath):
878 with open(fpath, 'rU') as f:
879 perspective = f.readline()
881 self._perspectives[name] = perspective
883 selected_perspective = self.gui.config['active perspective']
884 if not self._perspectives.has_key(selected_perspective):
885 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
887 self._restore_perspective(selected_perspective, force=True)
888 self._update_perspective_menu()
890 def _update_perspective_menu(self):
891 self._c['menu bar']._c['perspective'].update(
892 sorted(self._perspectives.keys()),
893 self.gui.config['active perspective'])
895 def _save_perspective(self, perspective, perspective_dir, name,
897 path = os.path.join(perspective_dir, name)
898 if extension != None:
900 if not os.path.isdir(perspective_dir):
901 os.makedirs(perspective_dir)
902 with open(path, 'w') as f:
904 self._perspectives[name] = perspective
905 self._restore_perspective(name)
906 self._update_perspective_menu()
908 def _delete_perspectives(self, perspective_dir, names,
910 self.log.debug('remove perspectives %s from %s'
911 % (names, perspective_dir))
913 path = os.path.join(perspective_dir, name)
914 if extension != None:
917 del(self._perspectives[name])
918 self._update_perspective_menu()
919 if self.gui.config['active perspective'] in names:
920 self._restore_perspective('Default')
921 # TODO: does this bug still apply?
922 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
923 # http://trac.wxwidgets.org/ticket/3258
924 # ) that makes the radio item indicator in the menu disappear.
925 # The code should be fine once this issue is fixed.
927 def _restore_perspective(self, name, force=False):
928 if name != self.gui.config['active perspective'] or force == True:
929 self.log.debug('restore perspective %s' % name)
930 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
931 self._c['manager'].LoadPerspective(self._perspectives[name])
932 self._c['manager'].Update()
933 for pane in self._c['manager'].GetAllPanes():
934 view = self._c['menu bar']._c['view']
935 if pane.name in view._c.keys():
936 view._c[pane.name].Check(pane.window.IsShown())
938 def _on_save_perspective(self, *args):
939 perspective = self._c['manager'].SavePerspective()
940 name = self.gui.config['active perspective']
941 if name == 'Default':
942 name = 'New perspective'
943 name = select_save_file(
944 directory=self.gui.config['perspective path'],
946 extension=self.gui.config['perspective extension'],
948 message='Enter a name for the new perspective:',
949 caption='Save perspective')
952 self._save_perspective(
953 perspective, self.gui.config['perspective path'], name=name,
954 extension=self.gui.config['perspective extension'])
956 def _on_delete_perspective(self, *args, **kwargs):
957 options = sorted([p for p in self._perspectives.keys()
959 dialog = SelectionDialog(
961 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
962 button_id=wx.ID_DELETE,
963 selection_style='multiple',
965 title='Delete perspective(s)',
966 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
967 dialog.CenterOnScreen()
969 if dialog.canceled == True:
971 names = [options[i] for i in dialog.selected]
973 self._delete_perspectives(
974 self.gui.config['perspective path'], names=names,
975 extension=self.gui.config['perspective extension'])
977 def _on_select_perspective(self, _class, method, name):
978 self._restore_perspective(name)
982 class HookeApp (wx.App):
983 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
985 Tosses up a splash screen and then loads :class:`HookeFrame` in
988 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
990 self.commands = commands
991 self.inqueue = inqueue
992 self.outqueue = outqueue
993 super(HookeApp, self).__init__(*args, **kwargs)
996 self.SetAppName('Hooke')
997 self.SetVendorName('')
998 self._setup_splash_screen()
1000 height = self.gui.config['main height']
1001 width = self.gui.config['main width']
1002 top = self.gui.config['main top']
1003 left = self.gui.config['main left']
1005 # Sometimes, the ini file gets confused and sets 'left' and
1006 # 'top' to large negative numbers. Here we catch and fix
1007 # this. Keep small negative numbers, the user might want
1015 'frame': HookeFrame(
1016 self.gui, self.commands, self.inqueue, self.outqueue,
1017 parent=None, title='Hooke',
1018 pos=(left, top), size=(width, height),
1019 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
1021 self._c['frame'].Show(True)
1022 self.SetTopWindow(self._c['frame'])
1025 def _setup_splash_screen(self):
1026 if self.gui.config['show splash screen'] == True:
1027 path = self.gui.config['splash screen image']
1028 if os.path.isfile(path):
1029 duration = self.gui.config['splash screen duration']
1031 bitmap=wx.Image(path).ConvertToBitmap(),
1032 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
1033 milliseconds=duration,
1036 # For some reason splashDuration and sleep do not
1037 # correspond to each other at least not on Windows.
1038 # Maybe it's because duration is in milliseconds and
1039 # sleep in seconds. Thus we need to increase the
1040 # sleep time a bit. A factor of 1.2 seems to work.
1042 time.sleep(sleepFactor * duration / 1000)
1045 class GUI (UserInterface):
1046 """wxWindows graphical user interface.
1049 super(GUI, self).__init__(name='gui')
1051 def default_settings(self):
1052 """Return a list of :class:`hooke.config.Setting`\s for any
1053 configurable UI settings.
1055 The suggested section setting is::
1057 Setting(section=self.setting_section, help=self.__doc__)
1060 Setting(section=self.setting_section, help=self.__doc__),
1061 Setting(section=self.setting_section, option='icon image',
1062 value=os.path.join('doc', 'img', 'microscope.ico'),
1064 help='Path to the hooke icon image.'),
1065 Setting(section=self.setting_section, option='show splash screen',
1066 value=True, type='bool',
1067 help='Enable/disable the splash screen'),
1068 Setting(section=self.setting_section, option='splash screen image',
1069 value=os.path.join('doc', 'img', 'hooke.jpg'),
1071 help='Path to the Hooke splash screen image.'),
1072 Setting(section=self.setting_section,
1073 option='splash screen duration',
1074 value=1000, type='int',
1075 help='Duration of the splash screen in milliseconds.'),
1076 Setting(section=self.setting_section, option='perspective path',
1077 value=os.path.join('resources', 'gui', 'perspective'),
1078 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1079 Setting(section=self.setting_section, option='perspective extension',
1081 help='Extension for perspective files.'),
1082 Setting(section=self.setting_section, option='hide extensions',
1083 value=False, type='bool',
1084 help='Hide file extensions when displaying names.'),
1085 Setting(section=self.setting_section, option='plot legend',
1086 value=True, type='bool',
1087 help='Enable/disable the plot legend.'),
1088 Setting(section=self.setting_section, option='plot SI format',
1089 value='True', type='bool',
1090 help='Enable/disable SI plot axes numbering.'),
1091 Setting(section=self.setting_section, option='plot decimals',
1092 value=2, type='int',
1093 help='Number of decimal places to show if "plot SI format" is enabled.'),
1094 Setting(section=self.setting_section, option='folders-workdir',
1095 value='.', type='path',
1096 help='This should probably go...'),
1097 Setting(section=self.setting_section, option='folders-filters',
1098 value='.', type='path',
1099 help='This should probably go...'),
1100 Setting(section=self.setting_section, option='active perspective',
1102 help='Name of active perspective file (or "Default").'),
1103 Setting(section=self.setting_section,
1104 option='folders-filter-index',
1105 value=0, type='int',
1106 help='This should probably go...'),
1107 Setting(section=self.setting_section, option='main height',
1108 value=450, type='int',
1109 help='Height of main window in pixels.'),
1110 Setting(section=self.setting_section, option='main width',
1111 value=800, type='int',
1112 help='Width of main window in pixels.'),
1113 Setting(section=self.setting_section, option='main top',
1114 value=0, type='int',
1115 help='Pixels from screen top to top of main window.'),
1116 Setting(section=self.setting_section, option='main left',
1117 value=0, type='int',
1118 help='Pixels from screen left to left of main window.'),
1119 Setting(section=self.setting_section, option='selected command',
1120 value='load playlist',
1121 help='Name of the initially selected command.'),
1124 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1128 app = HookeApp(gui=self,
1130 inqueue=ui_to_command_queue,
1131 outqueue=command_to_ui_queue,
1135 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1136 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)