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 ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
50 from ...ui import UserInterface, CommandMessage
51 from .dialog.selection import Selection as SelectionDialog
52 from .dialog.save_file import select_save_file
53 from . import menu as menu
54 from . import navbar as navbar
55 from . import panel as panel
56 from .panel.propertyeditor import props_from_argument, props_from_setting
57 from . import statusbar as statusbar
60 class HookeFrame (wx.Frame):
61 """The main Hooke-interface window.
63 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
64 super(HookeFrame, self).__init__(*args, **kwargs)
65 self.log = logging.getLogger('hooke')
67 self.commands = commands
68 self.inqueue = inqueue
69 self.outqueue = outqueue
70 self._perspectives = {} # {name: perspective_str}
73 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
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()
118 self.execute_command(
119 command=self._command_by_name('load playlist'),
120 args={'input':'test/data/test'},#vclamp_picoforce/playlist'},
122 self.execute_command(
123 command=self._command_by_name('load playlist'),
124 args={'input':'test/data/vclamp_picoforce/playlist'},
126 self.execute_command(
127 command=self._command_by_name('polymer fit'),
128 args={'block':1, 'bounds':[918, 1103]},
130 return # TODO: cleanup
131 self.playlists = self._c['playlist'].Playlists
132 self._displayed_plot = None
133 #load default list, if possible
134 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
139 def _setup_panels(self):
140 client_size = self.GetClientSize()
142 # ('folders', wx.GenericDirCtrl(
144 # dir=self.gui.config['folders-workdir'],
146 # style=wx.DIRCTRL_SHOW_FILTERS,
147 # filter=self.gui.config['folders-filters'],
148 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
149 (panel.PANELS['playlist'](
151 'delete_playlist':self._on_user_delete_playlist,
152 '_delete_playlist':self._on_delete_playlist,
153 'delete_curve':self._on_user_delete_curve,
154 '_delete_curve':self._on_delete_curve,
155 '_on_set_selected_playlist':self._on_set_selected_playlist,
156 '_on_set_selected_curve':self._on_set_selected_curve,
159 style=wx.WANTS_CHARS|wx.NO_BORDER,
160 # WANTS_CHARS so the panel doesn't eat the Return key.
163 (panel.PANELS['note'](
165 '_on_update':self._on_update_note,
168 style=wx.WANTS_CHARS|wx.NO_BORDER,
171 # ('notebook', Notebook(
173 # pos=wx.Point(client_size.x, client_size.y),
174 # size=wx.Size(430, 200),
175 # style=aui.AUI_NB_DEFAULT_STYLE
176 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
177 (panel.PANELS['commands'](
178 commands=self.commands,
179 selected=self.gui.config['selected command'],
181 'execute': self.execute_command,
182 'select_plugin': self.select_plugin,
183 'select_command': self.select_command,
184 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
187 style=wx.WANTS_CHARS|wx.NO_BORDER,
188 # WANTS_CHARS so the panel doesn't eat the Return key.
191 (panel.PANELS['propertyeditor'](
194 style=wx.WANTS_CHARS,
195 # WANTS_CHARS so the panel doesn't eat the Return key.
197 (panel.PANELS['plot'](
199 '_set_status_text': self._on_plot_status_text,
202 style=wx.WANTS_CHARS|wx.NO_BORDER,
203 # WANTS_CHARS so the panel doesn't eat the Return key.
206 (panel.PANELS['output'](
209 size=wx.Size(150, 90),
210 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
212 # ('results', panel.results.Results(self), 'bottom'),
214 self._add_panel(p, style)
216 def _add_panel(self, panel, style):
217 self._c[panel.name] = panel
218 m_name = panel.managed_name
219 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
220 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
223 elif style == 'center':
225 elif style == 'left':
227 elif style == 'right':
230 assert style == 'bottom', style
232 self._c['manager'].AddPane(panel, info)
234 def _setup_toolbars(self):
235 self._c['navigation bar'] = navbar.NavBar(
237 'next': self._next_curve,
238 'previous': self._previous_curve,
241 style=wx.TB_FLAT | wx.TB_NODIVIDER)
242 self._c['manager'].AddPane(
243 self._c['navigation bar'],
244 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
245 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
246 ).RightDockable(False))
248 def _bind_events(self):
249 # TODO: figure out if we can use the eventManager for menu
250 # ranges and events of 'self' without raising an assertion
252 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
253 self.Bind(wx.EVT_SIZE, self._on_size)
254 self.Bind(wx.EVT_CLOSE, self._on_close)
255 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
256 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
258 return # TODO: cleanup
259 treeCtrl = self._c['folders'].GetTreeCtrl()
260 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
263 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
265 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
267 def _on_about(self, *args):
268 dialog = wx.MessageDialog(
270 message=self.gui._splash_text(extra_info={
271 'get-details':'click "Help -> License"'},
273 caption='About Hooke',
274 style=wx.OK|wx.ICON_INFORMATION)
278 def _on_close(self, *args):
279 self.log.info('closing GUI framework')
281 self.gui.config['main height'] = str(self.GetSize().GetHeight())
282 self.gui.config['main left'] = str(self.GetPosition()[0])
283 self.gui.config['main top'] = str(self.GetPosition()[1])
284 self.gui.config['main width'] = str(self.GetSize().GetWidth())
285 # push changes back to Hooke.config?
286 self._c['manager'].UnInit()
287 del self._c['manager']
292 # Panel utility functions
294 def _file_name(self, name):
295 """Cleanup names according to configured preferences.
297 if self.gui.config['hide extensions'] == True:
298 name,ext = os.path.splitext(name)
305 def _command_by_name(self, name):
306 cs = [c for c in self.commands if c.name == name]
310 raise Exception('Multiple commands named "%s"' % name)
313 def execute_command(self, _class=None, method=None,
314 command=None, args=None):
317 if ('property editor' in self._c
318 and self.gui.config['selected command'] == command):
319 for name,value in self._c['property editor'].get_values().items():
320 arg = self._c['property editor']._argument_from_label.get(
325 args[arg.name] = value
327 # deal with counted arguments
328 if arg.name not in args:
330 index = int(name[len(arg.name):])
331 args[arg.name][index] = value
332 for arg in command.arguments:
334 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
335 count = arg._display_count
336 if count != 1 and arg.name in args:
337 keys = sorted(args[arg.name].keys())
338 assert keys == range(count), keys
339 args[arg.name] = [args[arg.name][i]
340 for i in range(count)]
341 self.log.debug('executing %s with %s' % (command.name, args))
342 self.inqueue.put(CommandMessage(command, args))
345 msg = self.outqueue.get()
347 if isinstance(msg, Exit):
350 elif isinstance(msg, CommandExit):
351 # TODO: display command complete
353 elif isinstance(msg, ReloadUserInterfaceConfig):
354 self.gui.reload_config(msg.config)
356 elif isinstance(msg, Request):
357 h = handler.HANDLERS[msg.type]
358 h.run(self, msg) # TODO: pause for response?
361 self, '_postprocess_%s' % command.name.replace(' ', '_'),
362 self._postprocess_text)
363 pp(command=command, args=args, results=results)
366 def _handle_request(self, msg):
367 """Repeatedly try to get a response to `msg`.
370 raise NotImplementedError('_%s_request_prompt' % msg.type)
371 prompt_string = prompt(msg)
372 parser = getattr(self, '_%s_request_parser' % msg.type, None)
374 raise NotImplementedError('_%s_request_parser' % msg.type)
378 self.cmd.stdout.write(''.join([
379 error.__class__.__name__, ': ', str(error), '\n']))
380 self.cmd.stdout.write(prompt_string)
381 value = parser(msg, self.cmd.stdin.readline())
383 response = msg.response(value)
385 except ValueError, error:
387 self.inqueue.put(response)
391 # Command-specific postprocessing
393 def _postprocess_text(self, command, args={}, results=[]):
394 """Print the string representation of the results to the Results window.
396 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
397 approach, except that :class:`~hooke.ui.commandline.DoCommand`
398 doesn't print some internally handled messages
399 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
401 for result in results:
402 if isinstance(result, CommandExit):
403 self._c['output'].write(result.__class__.__name__+'\n')
404 self._c['output'].write(str(result).rstrip()+'\n')
406 def _postprocess_load_playlist(self, command, args={}, results=None):
407 """Update `self` to show the playlist.
409 if not isinstance(results[-1], Success):
410 self._postprocess_text(command, results=results)
412 assert len(results) == 2, results
413 playlist = results[0]
414 self._c['playlist']._c['tree'].add_playlist(playlist)
416 def _postprocess_get_playlist(self, command, args={}, results=[]):
417 if not isinstance(results[-1], Success):
418 self._postprocess_text(command, results=results)
420 assert len(results) == 2, results
421 playlist = results[0]
422 self._c['playlist']._c['tree'].update_playlist(playlist)
424 def _postprocess_get_curve(self, command, args={}, results=[]):
425 """Update `self` to show the curve.
427 if not isinstance(results[-1], Success):
428 self._postprocess_text(command, results=results)
430 assert len(results) == 2, results
432 if args.get('curve', None) == None:
433 # the command defaults to the current curve of the current playlist
434 results = self.execute_command(
435 command=self._command_by_name('get playlist'))
436 playlist = results[0]
438 raise NotImplementedError()
439 if 'note' in self._c:
440 self._c['note'].set_text(curve.info['note'])
441 if 'playlist' in self._c:
442 self._c['playlist']._c['tree'].set_selected_curve(
444 if 'plot' in self._c:
445 self._c['plot'].set_curve(curve, config=self.gui.config)
447 def _postprocess_next_curve(self, command, args={}, results=[]):
448 """No-op. Only call 'next curve' via `self._next_curve()`.
452 def _postprocess_previous_curve(self, command, args={}, results=[]):
453 """No-op. Only call 'previous curve' via `self._previous_curve()`.
457 def _postprocess_zero_block_surface_contact_point(
458 self, command, args={}, results=[]):
459 """Update the curve, since the available columns may have changed.
461 if isinstance(results[-1], Success):
462 self.execute_command(
463 command=self._command_by_name('get curve'))
465 def _postprocess_add_block_force_array(
466 self, command, args={}, results=[]):
467 """Update the curve, since the available columns may have changed.
469 if isinstance(results[-1], Success):
470 self.execute_command(
471 command=self._command_by_name('get curve'))
477 def _GetActiveFileIndex(self):
478 lib.playlist.Playlist = self.GetActivePlaylist()
479 #get the selected item from the tree
480 selected_item = self._c['playlist']._c['tree'].GetSelection()
481 #test if a playlist or a curve was double-clicked
482 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
486 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
487 while selected_item.IsOk():
489 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
492 def _GetPlaylistTab(self, name):
493 for index, page in enumerate(self._c['notebook']._tabs._pages):
494 if page.caption == name:
498 def select_plugin(self, _class=None, method=None, plugin=None):
501 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
503 playlist = lib.playlist.Playlist(self, self.drivers)
505 playlist.add_curve(item)
506 if playlist.count > 0:
507 playlist.name = self._GetUniquePlaylistName(name)
509 self.AddTayliss(playlist)
511 def AppliesPlotmanipulator(self, name):
513 Returns True if the plotmanipulator 'name' is applied, False otherwise
514 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
516 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
518 def ApplyPlotmanipulators(self, plot, plot_file):
520 Apply all active plotmanipulators.
522 if plot is not None and plot_file is not None:
523 manipulated_plot = copy.deepcopy(plot)
524 for plotmanipulator in self.plotmanipulators:
525 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
526 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
527 return manipulated_plot
529 def GetActiveFigure(self):
530 playlist_name = self.GetActivePlaylistName()
531 figure = self.playlists[playlist_name].figure
532 if figure is not None:
536 def GetActiveFile(self):
537 playlist = self.GetActivePlaylist()
538 if playlist is not None:
539 return playlist.get_active_file()
542 def GetActivePlot(self):
543 playlist = self.GetActivePlaylist()
544 if playlist is not None:
545 return playlist.get_active_file().plot
548 def GetDisplayedPlot(self):
549 plot = copy.deepcopy(self.displayed_plot)
551 #plot.curves = copy.deepcopy(plot.curves)
554 def GetDisplayedPlotCorrected(self):
555 plot = copy.deepcopy(self.displayed_plot)
557 plot.curves = copy.deepcopy(plot.corrected_curves)
560 def GetDisplayedPlotRaw(self):
561 plot = copy.deepcopy(self.displayed_plot)
563 plot.curves = copy.deepcopy(plot.raw_curves)
566 def GetDockArt(self):
567 return self._c['manager'].GetArtProvider()
569 def GetPlotmanipulator(self, name):
571 Returns a plot manipulator function from its name
573 for plotmanipulator in self.plotmanipulators:
574 if plotmanipulator.name == name:
575 return plotmanipulator
578 def HasPlotmanipulator(self, name):
580 returns True if the plotmanipulator 'name' is loaded, False otherwise
582 for plotmanipulator in self.plotmanipulators:
583 if plotmanipulator.command == name:
588 def _on_dir_ctrl_left_double_click(self, event):
589 file_path = self.panelFolders.GetPath()
590 if os.path.isfile(file_path):
591 if file_path.endswith('.hkp'):
592 self.do_loadlist(file_path)
595 def _on_erase_background(self, event):
598 def _on_notebook_page_close(self, event):
599 ctrl = event.GetEventObject()
600 playlist_name = ctrl.GetPageText(ctrl._curpage)
601 self.DeleteFromPlaylists(playlist_name)
603 def OnPaneClose(self, event):
606 def OnPropGridChanged (self, event):
607 prop = event.GetProperty()
609 item_section = self.panelProperties.SelectedTreeItem
610 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
611 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
612 config = self.gui.config[plugin]
613 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
614 property_key = prop.GetName()
615 property_value = prop.GetDisplayedString()
617 config[property_section][property_key]['value'] = property_value
619 def OnResultsCheck(self, index, flag):
620 results = self.GetActivePlot().results
621 if results.has_key(self.results_str):
622 results[self.results_str].results[index].visible = flag
623 results[self.results_str].update()
627 def _on_size(self, event):
630 def UpdatePlaylistsTreeSelection(self):
631 playlist = self.GetActivePlaylist()
632 if playlist is not None:
633 if playlist.index >= 0:
634 self._c['status bar'].set_playlist(playlist)
638 def _on_curve_select(self, playlist, curve):
639 #create the plot tab and add playlist to the dictionary
640 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
641 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
642 #tab_index = self._c['notebook'].GetSelection()
643 playlist.figure = plotPanel.get_figure()
644 self.playlists[playlist.name] = playlist
645 #self.playlists[playlist.name] = [playlist, figure]
646 self._c['status bar'].set_playlist(playlist)
651 def _on_playlist_left_doubleclick(self):
652 index = self._c['notebook'].GetSelection()
653 current_playlist = self._c['notebook'].GetPageText(index)
654 if current_playlist != playlist_name:
655 index = self._GetPlaylistTab(playlist_name)
656 self._c['notebook'].SetSelection(index)
657 self._c['status bar'].set_playlist(playlist)
661 def _on_playlist_delete(self, playlist):
662 notebook = self.Parent.plotNotebook
663 index = self.Parent._GetPlaylistTab(playlist.name)
664 notebook.SetSelection(index)
665 notebook.DeletePage(notebook.GetSelection())
666 self.Parent.DeleteFromPlaylists(playlist_name)
670 # Command panel interface
672 def select_command(self, _class, method, command):
673 #self.select_plugin(plugin=command.plugin)
674 self._c['property editor'].clear()
675 self._c['property editor']._argument_from_label = {}
676 for argument in command.arguments:
677 if argument.name == 'help':
680 results = self.execute_command(
681 command=self._command_by_name('playlists'))
682 if not isinstance(results[-1], Success):
683 self._postprocess_text(command, results=results)
686 playlists = results[0]
688 results = self.execute_command(
689 command=self._command_by_name('playlist curves'))
690 if not isinstance(results[-1], Success):
691 self._postprocess_text(command, results=results)
696 ret = props_from_argument(
697 argument, curves=curves, playlists=playlists)
699 continue # property intentionally not handled (yet)
701 self._c['property editor'].append_property(p)
702 self._c['property editor']._argument_from_label[label] = (
705 self.gui.config['selected command'] = command # TODO: push to engine
709 # Note panel interface
711 def _on_update_note(self, _class, method, text):
712 """Sets the note for the active curve.
714 self.execute_command(
715 command=self._command_by_name('set note'),
720 # Playlist panel interface
722 def _on_user_delete_playlist(self, _class, method, playlist):
725 def _on_delete_playlist(self, _class, method, playlist):
726 if hasattr(playlist, 'path') and playlist.path != None:
727 os.remove(playlist.path)
729 def _on_user_delete_curve(self, _class, method, playlist, curve):
732 def _on_delete_curve(self, _class, method, playlist, curve):
733 # TODO: execute_command 'remove curve from playlist'
734 os.remove(curve.path)
736 def _on_set_selected_playlist(self, _class, method, playlist):
737 """Call the `jump to playlist` command.
739 results = self.execute_command(
740 command=self._command_by_name('playlists'))
741 if not isinstance(results[-1], Success):
743 assert len(results) == 2, results
744 playlists = results[0]
745 matching = [p for p in playlists if p.name == playlist.name]
746 assert len(matching) == 1, matching
747 index = playlists.index(matching[0])
748 results = self.execute_command(
749 command=self._command_by_name('jump to playlist'),
750 args={'index':index})
752 def _on_set_selected_curve(self, _class, method, playlist, curve):
753 """Call the `jump to curve` command.
755 self._on_set_selected_playlist(_class, method, playlist)
756 index = playlist.index(curve)
757 results = self.execute_command(
758 command=self._command_by_name('jump to curve'),
759 args={'index':index})
760 if not isinstance(results[-1], Success):
762 #results = self.execute_command(
763 # command=self._command_by_name('get playlist'))
764 #if not isinstance(results[-1], Success):
766 self.execute_command(
767 command=self._command_by_name('get curve'))
771 # Plot panel interface
773 def _on_plot_status_text(self, _class, method, text):
774 if 'status bar' in self._c:
775 self._c['status bar'].set_plot_text(text)
781 def _next_curve(self, *args):
782 """Call the `next curve` command.
784 results = self.execute_command(
785 command=self._command_by_name('next curve'))
786 if isinstance(results[-1], Success):
787 self.execute_command(
788 command=self._command_by_name('get curve'))
790 def _previous_curve(self, *args):
791 """Call the `previous curve` command.
793 results = self.execute_command(
794 command=self._command_by_name('previous curve'))
795 if isinstance(results[-1], Success):
796 self.execute_command(
797 command=self._command_by_name('get curve'))
801 # Panel display handling
803 def _on_panel_visibility(self, _class, method, panel_name, visible):
804 pane = self._c['manager'].GetPane(panel_name)
806 #if we don't do the following, the Folders pane does not resize properly on hide/show
807 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
808 #folders_size = pane.GetSize()
809 self.panelFolders.Fit()
810 self._c['manager'].Update()
812 def _setup_perspectives(self):
813 """Add perspectives to menubar and _perspectives.
815 self._perspectives = {
816 'Default': self._c['manager'].SavePerspective(),
818 path = self.gui.config['perspective path']
819 if os.path.isdir(path):
820 files = sorted(os.listdir(path))
822 name, extension = os.path.splitext(fname)
823 if extension != self.gui.config['perspective extension']:
825 fpath = os.path.join(path, fname)
826 if not os.path.isfile(fpath):
829 with open(fpath, 'rU') as f:
830 perspective = f.readline()
832 self._perspectives[name] = perspective
834 selected_perspective = self.gui.config['active perspective']
835 if not self._perspectives.has_key(selected_perspective):
836 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
838 self._restore_perspective(selected_perspective, force=True)
839 self._update_perspective_menu()
841 def _update_perspective_menu(self):
842 self._c['menu bar']._c['perspective'].update(
843 sorted(self._perspectives.keys()),
844 self.gui.config['active perspective'])
846 def _save_perspective(self, perspective, perspective_dir, name,
848 path = os.path.join(perspective_dir, name)
849 if extension != None:
851 if not os.path.isdir(perspective_dir):
852 os.makedirs(perspective_dir)
853 with open(path, 'w') as f:
855 self._perspectives[name] = perspective
856 self._restore_perspective(name)
857 self._update_perspective_menu()
859 def _delete_perspectives(self, perspective_dir, names,
861 self.log.debug('remove perspectives %s from %s'
862 % (names, perspective_dir))
864 path = os.path.join(perspective_dir, name)
865 if extension != None:
868 del(self._perspectives[name])
869 self._update_perspective_menu()
870 if self.gui.config['active perspective'] in names:
871 self._restore_perspective('Default')
872 # TODO: does this bug still apply?
873 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
874 # http://trac.wxwidgets.org/ticket/3258
875 # ) that makes the radio item indicator in the menu disappear.
876 # The code should be fine once this issue is fixed.
878 def _restore_perspective(self, name, force=False):
879 if name != self.gui.config['active perspective'] or force == True:
880 self.log.debug('restore perspective %s' % name)
881 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
882 self._c['manager'].LoadPerspective(self._perspectives[name])
883 self._c['manager'].Update()
884 for pane in self._c['manager'].GetAllPanes():
885 view = self._c['menu bar']._c['view']
886 if pane.name in view._c.keys():
887 view._c[pane.name].Check(pane.window.IsShown())
889 def _on_save_perspective(self, *args):
890 perspective = self._c['manager'].SavePerspective()
891 name = self.gui.config['active perspective']
892 if name == 'Default':
893 name = 'New perspective'
894 name = select_save_file(
895 directory=self.gui.config['perspective path'],
897 extension=self.gui.config['perspective extension'],
899 message='Enter a name for the new perspective:',
900 caption='Save perspective')
903 self._save_perspective(
904 perspective, self.gui.config['perspective path'], name=name,
905 extension=self.gui.config['perspective extension'])
907 def _on_delete_perspective(self, *args, **kwargs):
908 options = sorted([p for p in self._perspectives.keys()
910 dialog = SelectionDialog(
912 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
913 button_id=wx.ID_DELETE,
914 selection_style='multiple',
916 title='Delete perspective(s)',
917 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
918 dialog.CenterOnScreen()
920 if dialog.canceled == True:
922 names = [options[i] for i in dialog.selected]
924 self._delete_perspectives(
925 self.gui.config['perspective path'], names=names,
926 extension=self.gui.config['perspective extension'])
928 def _on_select_perspective(self, _class, method, name):
929 self._restore_perspective(name)
933 class HookeApp (wx.App):
934 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
936 Tosses up a splash screen and then loads :class:`HookeFrame` in
939 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
941 self.commands = commands
942 self.inqueue = inqueue
943 self.outqueue = outqueue
944 super(HookeApp, self).__init__(*args, **kwargs)
947 self.SetAppName('Hooke')
948 self.SetVendorName('')
949 self._setup_splash_screen()
951 height = self.gui.config['main height']
952 width = self.gui.config['main width']
953 top = self.gui.config['main top']
954 left = self.gui.config['main left']
956 # Sometimes, the ini file gets confused and sets 'left' and
957 # 'top' to large negative numbers. Here we catch and fix
958 # this. Keep small negative numbers, the user might want
967 self.gui, self.commands, self.inqueue, self.outqueue,
968 parent=None, title='Hooke',
969 pos=(left, top), size=(width, height),
970 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
972 self._c['frame'].Show(True)
973 self.SetTopWindow(self._c['frame'])
976 def _setup_splash_screen(self):
977 if self.gui.config['show splash screen'] == True:
978 path = self.gui.config['splash screen image']
979 if os.path.isfile(path):
980 duration = self.gui.config['splash screen duration']
982 bitmap=wx.Image(path).ConvertToBitmap(),
983 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
984 milliseconds=duration,
987 # For some reason splashDuration and sleep do not
988 # correspond to each other at least not on Windows.
989 # Maybe it's because duration is in milliseconds and
990 # sleep in seconds. Thus we need to increase the
991 # sleep time a bit. A factor of 1.2 seems to work.
993 time.sleep(sleepFactor * duration / 1000)
996 class GUI (UserInterface):
997 """wxWindows graphical user interface.
1000 super(GUI, self).__init__(name='gui')
1002 def default_settings(self):
1003 """Return a list of :class:`hooke.config.Setting`\s for any
1004 configurable UI settings.
1006 The suggested section setting is::
1008 Setting(section=self.setting_section, help=self.__doc__)
1011 Setting(section=self.setting_section, help=self.__doc__),
1012 Setting(section=self.setting_section, option='icon image',
1013 value=os.path.join('doc', 'img', 'microscope.ico'),
1015 help='Path to the hooke icon image.'),
1016 Setting(section=self.setting_section, option='show splash screen',
1017 value=True, type='bool',
1018 help='Enable/disable the splash screen'),
1019 Setting(section=self.setting_section, option='splash screen image',
1020 value=os.path.join('doc', 'img', 'hooke.jpg'),
1022 help='Path to the Hooke splash screen image.'),
1023 Setting(section=self.setting_section,
1024 option='splash screen duration',
1025 value=1000, type='int',
1026 help='Duration of the splash screen in milliseconds.'),
1027 Setting(section=self.setting_section, option='perspective path',
1028 value=os.path.join('resources', 'gui', 'perspective'),
1029 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1030 Setting(section=self.setting_section, option='perspective extension',
1032 help='Extension for perspective files.'),
1033 Setting(section=self.setting_section, option='hide extensions',
1034 value=False, type='bool',
1035 help='Hide file extensions when displaying names.'),
1036 Setting(section=self.setting_section, option='plot legend',
1037 value=True, type='bool',
1038 help='Enable/disable the plot legend.'),
1039 Setting(section=self.setting_section, option='plot SI format',
1040 value='True', type='bool',
1041 help='Enable/disable SI plot axes numbering.'),
1042 Setting(section=self.setting_section, option='plot decimals',
1043 value=2, type='int',
1044 help='Number of decimal places to show if "plot SI format" is enabled.'),
1045 Setting(section=self.setting_section, option='folders-workdir',
1046 value='.', type='path',
1047 help='This should probably go...'),
1048 Setting(section=self.setting_section, option='folders-filters',
1049 value='.', type='path',
1050 help='This should probably go...'),
1051 Setting(section=self.setting_section, option='active perspective',
1053 help='Name of active perspective file (or "Default").'),
1054 Setting(section=self.setting_section,
1055 option='folders-filter-index',
1056 value=0, type='int',
1057 help='This should probably go...'),
1058 Setting(section=self.setting_section, option='main height',
1059 value=450, type='int',
1060 help='Height of main window in pixels.'),
1061 Setting(section=self.setting_section, option='main width',
1062 value=800, type='int',
1063 help='Width of main window in pixels.'),
1064 Setting(section=self.setting_section, option='main top',
1065 value=0, type='int',
1066 help='Pixels from screen top to top of main window.'),
1067 Setting(section=self.setting_section, option='main left',
1068 value=0, type='int',
1069 help='Pixels from screen left to left of main window.'),
1070 Setting(section=self.setting_section, option='selected command',
1071 value='load playlist',
1072 help='Name of the initially selected command.'),
1075 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1079 app = HookeApp(gui=self,
1081 inqueue=ui_to_command_queue,
1082 outqueue=command_to_ui_queue,
1086 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1087 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)