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:
333 if arg.count != 1 and arg.name in args:
334 keys = sorted(args[arg.name].keys())
335 assert keys == range(arg.count), keys
336 args[arg.name] = [args[arg.name][i]
337 for i in range(arg.count)]
338 self.log.debug('executing %s with %s' % (command.name, args))
339 self.inqueue.put(CommandMessage(command, args))
342 msg = self.outqueue.get()
344 if isinstance(msg, Exit):
347 elif isinstance(msg, CommandExit):
348 # TODO: display command complete
350 elif isinstance(msg, ReloadUserInterfaceConfig):
351 self.gui.reload_config(msg.config)
353 elif isinstance(msg, Request):
354 h = handler.HANDLERS[msg.type]
355 h.run(self, msg) # TODO: pause for response?
358 self, '_postprocess_%s' % command.name.replace(' ', '_'),
359 self._postprocess_text)
360 pp(command=command, args=args, results=results)
363 def _handle_request(self, msg):
364 """Repeatedly try to get a response to `msg`.
367 raise NotImplementedError('_%s_request_prompt' % msg.type)
368 prompt_string = prompt(msg)
369 parser = getattr(self, '_%s_request_parser' % msg.type, None)
371 raise NotImplementedError('_%s_request_parser' % msg.type)
375 self.cmd.stdout.write(''.join([
376 error.__class__.__name__, ': ', str(error), '\n']))
377 self.cmd.stdout.write(prompt_string)
378 value = parser(msg, self.cmd.stdin.readline())
380 response = msg.response(value)
382 except ValueError, error:
384 self.inqueue.put(response)
388 # Command-specific postprocessing
390 def _postprocess_text(self, command, args={}, results=[]):
391 """Print the string representation of the results to the Results window.
393 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
394 approach, except that :class:`~hooke.ui.commandline.DoCommand`
395 doesn't print some internally handled messages
396 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
398 for result in results:
399 if isinstance(result, CommandExit):
400 self._c['output'].write(result.__class__.__name__+'\n')
401 self._c['output'].write(str(result).rstrip()+'\n')
403 def _postprocess_load_playlist(self, command, args={}, results=None):
404 """Update `self` to show the playlist.
406 if not isinstance(results[-1], Success):
407 self._postprocess_text(command, results=results)
409 assert len(results) == 2, results
410 playlist = results[0]
411 self._c['playlist']._c['tree'].add_playlist(playlist)
413 def _postprocess_get_playlist(self, command, args={}, results=[]):
414 if not isinstance(results[-1], Success):
415 self._postprocess_text(command, results=results)
417 assert len(results) == 2, results
418 playlist = results[0]
419 self._c['playlist']._c['tree'].update_playlist(playlist)
421 def _postprocess_get_curve(self, command, args={}, results=[]):
422 """Update `self` to show the curve.
424 if not isinstance(results[-1], Success):
425 self._postprocess_text(command, results=results)
427 assert len(results) == 2, results
429 if args.get('curve', None) == None:
430 # the command defaults to the current curve of the current playlist
431 results = self.execute_command(
432 command=self._command_by_name('get playlist'))
433 playlist = results[0]
435 raise NotImplementedError()
436 if 'note' in self._c:
437 self._c['note'].set_text(curve.info['note'])
438 if 'playlist' in self._c:
439 self._c['playlist']._c['tree'].set_selected_curve(
441 if 'plot' in self._c:
442 self._c['plot'].set_curve(curve, config=self.gui.config)
444 def _postprocess_next_curve(self, command, args={}, results=[]):
445 """No-op. Only call 'next curve' via `self._next_curve()`.
449 def _postprocess_previous_curve(self, command, args={}, results=[]):
450 """No-op. Only call 'previous curve' via `self._previous_curve()`.
454 def _postprocess_zero_block_surface_contact_point(
455 self, command, args={}, results=[]):
456 """Update the curve, since the available columns may have changed.
458 if isinstance(results[-1], Success):
459 self.execute_command(
460 command=self._command_by_name('get curve'))
462 def _postprocess_add_block_force_array(
463 self, command, args={}, results=[]):
464 """Update the curve, since the available columns may have changed.
466 if isinstance(results[-1], Success):
467 self.execute_command(
468 command=self._command_by_name('get curve'))
474 def _GetActiveFileIndex(self):
475 lib.playlist.Playlist = self.GetActivePlaylist()
476 #get the selected item from the tree
477 selected_item = self._c['playlist']._c['tree'].GetSelection()
478 #test if a playlist or a curve was double-clicked
479 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
483 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
484 while selected_item.IsOk():
486 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
489 def _GetPlaylistTab(self, name):
490 for index, page in enumerate(self._c['notebook']._tabs._pages):
491 if page.caption == name:
495 def select_plugin(self, _class=None, method=None, plugin=None):
498 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
500 playlist = lib.playlist.Playlist(self, self.drivers)
502 playlist.add_curve(item)
503 if playlist.count > 0:
504 playlist.name = self._GetUniquePlaylistName(name)
506 self.AddTayliss(playlist)
508 def AppliesPlotmanipulator(self, name):
510 Returns True if the plotmanipulator 'name' is applied, False otherwise
511 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
513 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
515 def ApplyPlotmanipulators(self, plot, plot_file):
517 Apply all active plotmanipulators.
519 if plot is not None and plot_file is not None:
520 manipulated_plot = copy.deepcopy(plot)
521 for plotmanipulator in self.plotmanipulators:
522 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
523 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
524 return manipulated_plot
526 def GetActiveFigure(self):
527 playlist_name = self.GetActivePlaylistName()
528 figure = self.playlists[playlist_name].figure
529 if figure is not None:
533 def GetActiveFile(self):
534 playlist = self.GetActivePlaylist()
535 if playlist is not None:
536 return playlist.get_active_file()
539 def GetActivePlot(self):
540 playlist = self.GetActivePlaylist()
541 if playlist is not None:
542 return playlist.get_active_file().plot
545 def GetDisplayedPlot(self):
546 plot = copy.deepcopy(self.displayed_plot)
548 #plot.curves = copy.deepcopy(plot.curves)
551 def GetDisplayedPlotCorrected(self):
552 plot = copy.deepcopy(self.displayed_plot)
554 plot.curves = copy.deepcopy(plot.corrected_curves)
557 def GetDisplayedPlotRaw(self):
558 plot = copy.deepcopy(self.displayed_plot)
560 plot.curves = copy.deepcopy(plot.raw_curves)
563 def GetDockArt(self):
564 return self._c['manager'].GetArtProvider()
566 def GetPlotmanipulator(self, name):
568 Returns a plot manipulator function from its name
570 for plotmanipulator in self.plotmanipulators:
571 if plotmanipulator.name == name:
572 return plotmanipulator
575 def HasPlotmanipulator(self, name):
577 returns True if the plotmanipulator 'name' is loaded, False otherwise
579 for plotmanipulator in self.plotmanipulators:
580 if plotmanipulator.command == name:
585 def _on_dir_ctrl_left_double_click(self, event):
586 file_path = self.panelFolders.GetPath()
587 if os.path.isfile(file_path):
588 if file_path.endswith('.hkp'):
589 self.do_loadlist(file_path)
592 def _on_erase_background(self, event):
595 def _on_notebook_page_close(self, event):
596 ctrl = event.GetEventObject()
597 playlist_name = ctrl.GetPageText(ctrl._curpage)
598 self.DeleteFromPlaylists(playlist_name)
600 def OnPaneClose(self, event):
603 def OnPropGridChanged (self, event):
604 prop = event.GetProperty()
606 item_section = self.panelProperties.SelectedTreeItem
607 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
608 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
609 config = self.gui.config[plugin]
610 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
611 property_key = prop.GetName()
612 property_value = prop.GetDisplayedString()
614 config[property_section][property_key]['value'] = property_value
616 def OnResultsCheck(self, index, flag):
617 results = self.GetActivePlot().results
618 if results.has_key(self.results_str):
619 results[self.results_str].results[index].visible = flag
620 results[self.results_str].update()
624 def _on_size(self, event):
627 def UpdatePlaylistsTreeSelection(self):
628 playlist = self.GetActivePlaylist()
629 if playlist is not None:
630 if playlist.index >= 0:
631 self._c['status bar'].set_playlist(playlist)
635 def _on_curve_select(self, playlist, curve):
636 #create the plot tab and add playlist to the dictionary
637 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
638 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
639 #tab_index = self._c['notebook'].GetSelection()
640 playlist.figure = plotPanel.get_figure()
641 self.playlists[playlist.name] = playlist
642 #self.playlists[playlist.name] = [playlist, figure]
643 self._c['status bar'].set_playlist(playlist)
648 def _on_playlist_left_doubleclick(self):
649 index = self._c['notebook'].GetSelection()
650 current_playlist = self._c['notebook'].GetPageText(index)
651 if current_playlist != playlist_name:
652 index = self._GetPlaylistTab(playlist_name)
653 self._c['notebook'].SetSelection(index)
654 self._c['status bar'].set_playlist(playlist)
658 def _on_playlist_delete(self, playlist):
659 notebook = self.Parent.plotNotebook
660 index = self.Parent._GetPlaylistTab(playlist.name)
661 notebook.SetSelection(index)
662 notebook.DeletePage(notebook.GetSelection())
663 self.Parent.DeleteFromPlaylists(playlist_name)
667 # Command panel interface
669 def select_command(self, _class, method, command):
670 #self.select_plugin(plugin=command.plugin)
671 self._c['property editor'].clear()
672 self._c['property editor']._argument_from_label = {}
673 for argument in command.arguments:
674 if argument.name == 'help':
677 results = self.execute_command(
678 command=self._command_by_name('playlists'))
679 if not isinstance(results[-1], Success):
680 self._postprocess_text(command, results=results)
683 playlists = results[0]
685 results = self.execute_command(
686 command=self._command_by_name('playlist curves'))
687 if not isinstance(results[-1], Success):
688 self._postprocess_text(command, results=results)
693 ret = props_from_argument(
694 argument, curves=curves, playlists=playlists)
696 continue # property intentionally not handled (yet)
698 self._c['property editor'].append_property(p)
699 self._c['property editor']._argument_from_label[label] = (
702 self.gui.config['selected command'] = command # TODO: push to engine
706 # Note panel interface
708 def _on_update_note(self, _class, method, text):
709 """Sets the note for the active curve.
711 self.execute_command(
712 command=self._command_by_name('set note'),
717 # Playlist panel interface
719 def _on_user_delete_playlist(self, _class, method, playlist):
722 def _on_delete_playlist(self, _class, method, playlist):
723 if hasattr(playlist, 'path') and playlist.path != None:
724 os.remove(playlist.path)
726 def _on_user_delete_curve(self, _class, method, playlist, curve):
729 def _on_delete_curve(self, _class, method, playlist, curve):
730 # TODO: execute_command 'remove curve from playlist'
731 os.remove(curve.path)
733 def _on_set_selected_playlist(self, _class, method, playlist):
734 """Call the `jump to playlist` command.
736 results = self.execute_command(
737 command=self._command_by_name('playlists'))
738 if not isinstance(results[-1], Success):
740 assert len(results) == 2, results
741 playlists = results[0]
742 matching = [p for p in playlists if p.name == playlist.name]
743 assert len(matching) == 1, matching
744 index = playlists.index(matching[0])
745 results = self.execute_command(
746 command=self._command_by_name('jump to playlist'),
747 args={'index':index})
749 def _on_set_selected_curve(self, _class, method, playlist, curve):
750 """Call the `jump to curve` command.
752 self._on_set_selected_playlist(_class, method, playlist)
753 index = playlist.index(curve)
754 results = self.execute_command(
755 command=self._command_by_name('jump to curve'),
756 args={'index':index})
757 if not isinstance(results[-1], Success):
759 #results = self.execute_command(
760 # command=self._command_by_name('get playlist'))
761 #if not isinstance(results[-1], Success):
763 self.execute_command(
764 command=self._command_by_name('get curve'))
768 # Plot panel interface
770 def _on_plot_status_text(self, _class, method, text):
771 if 'status bar' in self._c:
772 self._c['status bar'].set_plot_text(text)
778 def _next_curve(self, *args):
779 """Call the `next curve` command.
781 results = self.execute_command(
782 command=self._command_by_name('next curve'))
783 if isinstance(results[-1], Success):
784 self.execute_command(
785 command=self._command_by_name('get curve'))
787 def _previous_curve(self, *args):
788 """Call the `previous curve` command.
790 results = self.execute_command(
791 command=self._command_by_name('previous curve'))
792 if isinstance(results[-1], Success):
793 self.execute_command(
794 command=self._command_by_name('get curve'))
798 # Panel display handling
800 def _on_panel_visibility(self, _class, method, panel_name, visible):
801 pane = self._c['manager'].GetPane(panel_name)
803 #if we don't do the following, the Folders pane does not resize properly on hide/show
804 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
805 #folders_size = pane.GetSize()
806 self.panelFolders.Fit()
807 self._c['manager'].Update()
809 def _setup_perspectives(self):
810 """Add perspectives to menubar and _perspectives.
812 self._perspectives = {
813 'Default': self._c['manager'].SavePerspective(),
815 path = self.gui.config['perspective path']
816 if os.path.isdir(path):
817 files = sorted(os.listdir(path))
819 name, extension = os.path.splitext(fname)
820 if extension != self.gui.config['perspective extension']:
822 fpath = os.path.join(path, fname)
823 if not os.path.isfile(fpath):
826 with open(fpath, 'rU') as f:
827 perspective = f.readline()
829 self._perspectives[name] = perspective
831 selected_perspective = self.gui.config['active perspective']
832 if not self._perspectives.has_key(selected_perspective):
833 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
835 self._restore_perspective(selected_perspective, force=True)
836 self._update_perspective_menu()
838 def _update_perspective_menu(self):
839 self._c['menu bar']._c['perspective'].update(
840 sorted(self._perspectives.keys()),
841 self.gui.config['active perspective'])
843 def _save_perspective(self, perspective, perspective_dir, name,
845 path = os.path.join(perspective_dir, name)
846 if extension != None:
848 if not os.path.isdir(perspective_dir):
849 os.makedirs(perspective_dir)
850 with open(path, 'w') as f:
852 self._perspectives[name] = perspective
853 self._restore_perspective(name)
854 self._update_perspective_menu()
856 def _delete_perspectives(self, perspective_dir, names,
858 self.log.debug('remove perspectives %s from %s'
859 % (names, perspective_dir))
861 path = os.path.join(perspective_dir, name)
862 if extension != None:
865 del(self._perspectives[name])
866 self._update_perspective_menu()
867 if self.gui.config['active perspective'] in names:
868 self._restore_perspective('Default')
869 # TODO: does this bug still apply?
870 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
871 # http://trac.wxwidgets.org/ticket/3258
872 # ) that makes the radio item indicator in the menu disappear.
873 # The code should be fine once this issue is fixed.
875 def _restore_perspective(self, name, force=False):
876 if name != self.gui.config['active perspective'] or force == True:
877 self.log.debug('restore perspective %s' % name)
878 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
879 self._c['manager'].LoadPerspective(self._perspectives[name])
880 self._c['manager'].Update()
881 for pane in self._c['manager'].GetAllPanes():
882 view = self._c['menu bar']._c['view']
883 if pane.name in view._c.keys():
884 view._c[pane.name].Check(pane.window.IsShown())
886 def _on_save_perspective(self, *args):
887 perspective = self._c['manager'].SavePerspective()
888 name = self.gui.config['active perspective']
889 if name == 'Default':
890 name = 'New perspective'
891 name = select_save_file(
892 directory=self.gui.config['perspective path'],
894 extension=self.gui.config['perspective extension'],
896 message='Enter a name for the new perspective:',
897 caption='Save perspective')
900 self._save_perspective(
901 perspective, self.gui.config['perspective path'], name=name,
902 extension=self.gui.config['perspective extension'])
904 def _on_delete_perspective(self, *args, **kwargs):
905 options = sorted([p for p in self._perspectives.keys()
907 dialog = SelectionDialog(
909 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
910 button_id=wx.ID_DELETE,
911 selection_style='multiple',
913 title='Delete perspective(s)',
914 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
915 dialog.CenterOnScreen()
917 if dialog.canceled == True:
919 names = [options[i] for i in dialog.selected]
921 self._delete_perspectives(
922 self.gui.config['perspective path'], names=names,
923 extension=self.gui.config['perspective extension'])
925 def _on_select_perspective(self, _class, method, name):
926 self._restore_perspective(name)
930 class HookeApp (wx.App):
931 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
933 Tosses up a splash screen and then loads :class:`HookeFrame` in
936 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
938 self.commands = commands
939 self.inqueue = inqueue
940 self.outqueue = outqueue
941 super(HookeApp, self).__init__(*args, **kwargs)
944 self.SetAppName('Hooke')
945 self.SetVendorName('')
946 self._setup_splash_screen()
948 height = self.gui.config['main height']
949 width = self.gui.config['main width']
950 top = self.gui.config['main top']
951 left = self.gui.config['main left']
953 # Sometimes, the ini file gets confused and sets 'left' and
954 # 'top' to large negative numbers. Here we catch and fix
955 # this. Keep small negative numbers, the user might want
964 self.gui, self.commands, self.inqueue, self.outqueue,
965 parent=None, title='Hooke',
966 pos=(left, top), size=(width, height),
967 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
969 self._c['frame'].Show(True)
970 self.SetTopWindow(self._c['frame'])
973 def _setup_splash_screen(self):
974 if self.gui.config['show splash screen'] == True:
975 path = self.gui.config['splash screen image']
976 if os.path.isfile(path):
977 duration = self.gui.config['splash screen duration']
979 bitmap=wx.Image(path).ConvertToBitmap(),
980 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
981 milliseconds=duration,
984 # For some reason splashDuration and sleep do not
985 # correspond to each other at least not on Windows.
986 # Maybe it's because duration is in milliseconds and
987 # sleep in seconds. Thus we need to increase the
988 # sleep time a bit. A factor of 1.2 seems to work.
990 time.sleep(sleepFactor * duration / 1000)
993 class GUI (UserInterface):
994 """wxWindows graphical user interface.
997 super(GUI, self).__init__(name='gui')
999 def default_settings(self):
1000 """Return a list of :class:`hooke.config.Setting`\s for any
1001 configurable UI settings.
1003 The suggested section setting is::
1005 Setting(section=self.setting_section, help=self.__doc__)
1008 Setting(section=self.setting_section, help=self.__doc__),
1009 Setting(section=self.setting_section, option='icon image',
1010 value=os.path.join('doc', 'img', 'microscope.ico'),
1012 help='Path to the hooke icon image.'),
1013 Setting(section=self.setting_section, option='show splash screen',
1014 value=True, type='bool',
1015 help='Enable/disable the splash screen'),
1016 Setting(section=self.setting_section, option='splash screen image',
1017 value=os.path.join('doc', 'img', 'hooke.jpg'),
1019 help='Path to the Hooke splash screen image.'),
1020 Setting(section=self.setting_section,
1021 option='splash screen duration',
1022 value=1000, type='int',
1023 help='Duration of the splash screen in milliseconds.'),
1024 Setting(section=self.setting_section, option='perspective path',
1025 value=os.path.join('resources', 'gui', 'perspective'),
1026 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1027 Setting(section=self.setting_section, option='perspective extension',
1029 help='Extension for perspective files.'),
1030 Setting(section=self.setting_section, option='hide extensions',
1031 value=False, type='bool',
1032 help='Hide file extensions when displaying names.'),
1033 Setting(section=self.setting_section, option='plot legend',
1034 value=True, type='bool',
1035 help='Enable/disable the plot legend.'),
1036 Setting(section=self.setting_section, option='plot SI format',
1037 value='True', type='bool',
1038 help='Enable/disable SI plot axes numbering.'),
1039 Setting(section=self.setting_section, option='plot decimals',
1040 value=2, type='int',
1041 help='Number of decimal places to show if "plot SI format" is enabled.'),
1042 Setting(section=self.setting_section, option='folders-workdir',
1043 value='.', type='path',
1044 help='This should probably go...'),
1045 Setting(section=self.setting_section, option='folders-filters',
1046 value='.', type='path',
1047 help='This should probably go...'),
1048 Setting(section=self.setting_section, option='active perspective',
1050 help='Name of active perspective file (or "Default").'),
1051 Setting(section=self.setting_section,
1052 option='folders-filter-index',
1053 value=0, type='int',
1054 help='This should probably go...'),
1055 Setting(section=self.setting_section, option='main height',
1056 value=450, type='int',
1057 help='Height of main window in pixels.'),
1058 Setting(section=self.setting_section, option='main width',
1059 value=800, type='int',
1060 help='Width of main window in pixels.'),
1061 Setting(section=self.setting_section, option='main top',
1062 value=0, type='int',
1063 help='Pixels from screen top to top of main window.'),
1064 Setting(section=self.setting_section, option='main left',
1065 value=0, type='int',
1066 help='Pixels from screen left to left of main window.'),
1067 Setting(section=self.setting_section, option='selected command',
1068 value='load playlist',
1069 help='Name of the initially selected command.'),
1072 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1076 app = HookeApp(gui=self,
1078 inqueue=ui_to_command_queue,
1079 outqueue=command_to_ui_queue,
1083 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1084 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)