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 return # TODO: cleanup
127 self.playlists = self._c['playlist'].Playlists
128 self._displayed_plot = None
129 #load default list, if possible
130 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
135 def _setup_panels(self):
136 client_size = self.GetClientSize()
138 # ('folders', wx.GenericDirCtrl(
140 # dir=self.gui.config['folders-workdir'],
142 # style=wx.DIRCTRL_SHOW_FILTERS,
143 # filter=self.gui.config['folders-filters'],
144 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
145 (panel.PANELS['playlist'](
147 'delete_playlist':self._on_user_delete_playlist,
148 '_delete_playlist':self._on_delete_playlist,
149 'delete_curve':self._on_user_delete_curve,
150 '_delete_curve':self._on_delete_curve,
151 '_on_set_selected_playlist':self._on_set_selected_playlist,
152 '_on_set_selected_curve':self._on_set_selected_curve,
155 style=wx.WANTS_CHARS|wx.NO_BORDER,
156 # WANTS_CHARS so the panel doesn't eat the Return key.
159 (panel.PANELS['note'](
161 '_on_update':self._on_update_note,
164 style=wx.WANTS_CHARS|wx.NO_BORDER,
167 # ('notebook', Notebook(
169 # pos=wx.Point(client_size.x, client_size.y),
170 # size=wx.Size(430, 200),
171 # style=aui.AUI_NB_DEFAULT_STYLE
172 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
173 (panel.PANELS['commands'](
174 commands=self.commands,
175 selected=self.gui.config['selected command'],
177 'execute': self.execute_command,
178 'select_plugin': self.select_plugin,
179 'select_command': self.select_command,
180 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
183 style=wx.WANTS_CHARS|wx.NO_BORDER,
184 # WANTS_CHARS so the panel doesn't eat the Return key.
187 (panel.PANELS['propertyeditor'](
190 style=wx.WANTS_CHARS,
191 # WANTS_CHARS so the panel doesn't eat the Return key.
193 # ('assistant', wx.TextCtrl(
195 # pos=wx.Point(0, 0),
196 # size=wx.Size(150, 90),
197 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
198 (panel.PANELS['plot'](
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)
215 #self._c['assistant'].SetEditable(False)
217 def _add_panel(self, panel, style):
218 self._c[panel.name] = panel
219 m_name = panel.managed_name
220 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
221 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
224 elif style == 'center':
226 elif style == 'left':
228 elif style == 'right':
231 assert style == 'bottom', style
233 self._c['manager'].AddPane(panel, info)
235 def _setup_toolbars(self):
236 self._c['navigation bar'] = navbar.NavBar(
238 'next': self._next_curve,
239 'previous': self._previous_curve,
242 style=wx.TB_FLAT | wx.TB_NODIVIDER)
243 self._c['manager'].AddPane(
244 self._c['navigation bar'],
245 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
246 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
247 ).RightDockable(False))
249 def _bind_events(self):
250 # TODO: figure out if we can use the eventManager for menu
251 # ranges and events of 'self' without raising an assertion
253 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
254 self.Bind(wx.EVT_SIZE, self._on_size)
255 self.Bind(wx.EVT_CLOSE, self._on_close)
256 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
257 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
259 return # TODO: cleanup
260 treeCtrl = self._c['folders'].GetTreeCtrl()
261 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
264 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
266 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
268 def _on_about(self, *args):
269 dialog = wx.MessageDialog(
271 message=self.gui._splash_text(extra_info={
272 'get-details':'click "Help -> License"'},
274 caption='About Hooke',
275 style=wx.OK|wx.ICON_INFORMATION)
279 def _on_close(self, *args):
280 self.log.info('closing GUI framework')
282 self.gui.config['main height'] = str(self.GetSize().GetHeight())
283 self.gui.config['main left'] = str(self.GetPosition()[0])
284 self.gui.config['main top'] = str(self.GetPosition()[1])
285 self.gui.config['main width'] = str(self.GetSize().GetWidth())
286 # push changes back to Hooke.config?
287 self._c['manager'].UnInit()
288 del self._c['manager']
293 # Panel utility functions
295 def _file_name(self, name):
296 """Cleanup names according to configured preferences.
298 if self.gui.config['hide extensions'] == True:
299 name,ext = os.path.splitext(name)
306 def _command_by_name(self, name):
307 cs = [c for c in self.commands if c.name == name]
311 raise Exception('Multiple commands named "%s"' % name)
314 def execute_command(self, _class=None, method=None,
315 command=None, args=None):
318 if ('property editor' in self._c
319 and self.gui.config['selected command'] == command):
320 for name,value in self._c['property editor'].get_values().items():
321 arg = self._c['property editor']._argument_from_label.get(
326 args[arg.name] = value
328 # deal with counted arguments
329 if arg.name not in args:
331 index = int(name[len(arg.name):])
332 args[arg.name][index] = value
333 for arg in command.arguments:
334 if arg.count != 1 and arg.name in args:
335 keys = sorted(args[arg.name].keys())
336 assert keys == range(arg.count), keys
337 args[arg.name] = [args[arg.name][i]
338 for i in range(arg.count)]
339 self.log.debug('executing %s with %s' % (command.name, args))
340 self.inqueue.put(CommandMessage(command, args))
343 msg = self.outqueue.get()
345 if isinstance(msg, Exit):
348 elif isinstance(msg, CommandExit):
349 # TODO: display command complete
351 elif isinstance(msg, ReloadUserInterfaceConfig):
352 self.gui.reload_config(msg.config)
354 elif isinstance(msg, Request):
355 h = handler.HANDLERS[msg.type]
356 h.run(self, msg) # TODO: pause for response?
359 self, '_postprocess_%s' % command.name.replace(' ', '_'),
360 self._postprocess_text)
361 pp(command=command, args=args, results=results)
364 def _handle_request(self, msg):
365 """Repeatedly try to get a response to `msg`.
368 raise NotImplementedError('_%s_request_prompt' % msg.type)
369 prompt_string = prompt(msg)
370 parser = getattr(self, '_%s_request_parser' % msg.type, None)
372 raise NotImplementedError('_%s_request_parser' % msg.type)
376 self.cmd.stdout.write(''.join([
377 error.__class__.__name__, ': ', str(error), '\n']))
378 self.cmd.stdout.write(prompt_string)
379 value = parser(msg, self.cmd.stdin.readline())
381 response = msg.response(value)
383 except ValueError, error:
385 self.inqueue.put(response)
389 # Command-specific postprocessing
391 def _postprocess_text(self, command, args={}, results=[]):
392 """Print the string representation of the results to the Results window.
394 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
395 approach, except that :class:`~hooke.ui.commandline.DoCommand`
396 doesn't print some internally handled messages
397 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
399 for result in results:
400 if isinstance(result, CommandExit):
401 self._c['output'].write(result.__class__.__name__+'\n')
402 self._c['output'].write(str(result).rstrip()+'\n')
404 def _postprocess_load_playlist(self, command, args={}, results=None):
405 """Update `self` to show the playlist.
407 if not isinstance(results[-1], Success):
408 self._postprocess_text(command, results=results)
410 assert len(results) == 2, results
411 playlist = results[0]
412 self._c['playlist']._c['tree'].add_playlist(playlist)
414 def _postprocess_get_playlist(self, command, args={}, results=[]):
415 if not isinstance(results[-1], Success):
416 self._postprocess_text(command, results=results)
418 assert len(results) == 2, results
419 playlist = results[0]
420 self._c['playlist']._c['tree'].update_playlist(playlist)
422 def _postprocess_get_curve(self, command, args={}, results=[]):
423 """Update `self` to show the curve.
425 if not isinstance(results[-1], Success):
426 self._postprocess_text(command, results=results)
428 assert len(results) == 2, results
430 if args.get('curve', None) == None:
431 # the command defaults to the current curve of the current playlist
432 results = self.execute_command(
433 command=self._command_by_name('get playlist'))
434 playlist = results[0]
436 raise NotImplementedError()
437 if 'note' in self._c:
438 self._c['note'].set_text(curve.info['note'])
439 if 'playlist' in self._c:
440 self._c['playlist']._c['tree'].set_selected_curve(
442 if 'plot' in self._c:
443 self._c['plot'].set_curve(curve, config=self.gui.config)
445 def _postprocess_next_curve(self, command, args={}, results=[]):
446 """No-op. Only call 'next curve' via `self._next_curve()`.
450 def _postprocess_previous_curve(self, command, args={}, results=[]):
451 """No-op. Only call 'previous curve' via `self._previous_curve()`.
455 def _postprocess_zero_block_surface_contact_point(
456 self, command, args={}, results=[]):
457 """Update the curve, since the available columns may have changed.
459 if isinstance(results[-1], Success):
460 self.execute_command(
461 command=self._command_by_name('get curve'))
463 def _postprocess_add_block_force_array(
464 self, command, args={}, results=[]):
465 """Update the curve, since the available columns may have changed.
467 if isinstance(results[-1], Success):
468 self.execute_command(
469 command=self._command_by_name('get curve'))
475 def _GetActiveFileIndex(self):
476 lib.playlist.Playlist = self.GetActivePlaylist()
477 #get the selected item from the tree
478 selected_item = self._c['playlist']._c['tree'].GetSelection()
479 #test if a playlist or a curve was double-clicked
480 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
484 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
485 while selected_item.IsOk():
487 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
490 def _GetPlaylistTab(self, name):
491 for index, page in enumerate(self._c['notebook']._tabs._pages):
492 if page.caption == name:
496 def select_plugin(self, _class=None, method=None, plugin=None):
499 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
501 playlist = lib.playlist.Playlist(self, self.drivers)
503 playlist.add_curve(item)
504 if playlist.count > 0:
505 playlist.name = self._GetUniquePlaylistName(name)
507 self.AddTayliss(playlist)
509 def AppliesPlotmanipulator(self, name):
511 Returns True if the plotmanipulator 'name' is applied, False otherwise
512 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
514 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
516 def ApplyPlotmanipulators(self, plot, plot_file):
518 Apply all active plotmanipulators.
520 if plot is not None and plot_file is not None:
521 manipulated_plot = copy.deepcopy(plot)
522 for plotmanipulator in self.plotmanipulators:
523 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
524 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
525 return manipulated_plot
527 def GetActiveFigure(self):
528 playlist_name = self.GetActivePlaylistName()
529 figure = self.playlists[playlist_name].figure
530 if figure is not None:
534 def GetActiveFile(self):
535 playlist = self.GetActivePlaylist()
536 if playlist is not None:
537 return playlist.get_active_file()
540 def GetActivePlot(self):
541 playlist = self.GetActivePlaylist()
542 if playlist is not None:
543 return playlist.get_active_file().plot
546 def GetDisplayedPlot(self):
547 plot = copy.deepcopy(self.displayed_plot)
549 #plot.curves = copy.deepcopy(plot.curves)
552 def GetDisplayedPlotCorrected(self):
553 plot = copy.deepcopy(self.displayed_plot)
555 plot.curves = copy.deepcopy(plot.corrected_curves)
558 def GetDisplayedPlotRaw(self):
559 plot = copy.deepcopy(self.displayed_plot)
561 plot.curves = copy.deepcopy(plot.raw_curves)
564 def GetDockArt(self):
565 return self._c['manager'].GetArtProvider()
567 def GetPlotmanipulator(self, name):
569 Returns a plot manipulator function from its name
571 for plotmanipulator in self.plotmanipulators:
572 if plotmanipulator.name == name:
573 return plotmanipulator
576 def HasPlotmanipulator(self, name):
578 returns True if the plotmanipulator 'name' is loaded, False otherwise
580 for plotmanipulator in self.plotmanipulators:
581 if plotmanipulator.command == name:
586 def _on_dir_ctrl_left_double_click(self, event):
587 file_path = self.panelFolders.GetPath()
588 if os.path.isfile(file_path):
589 if file_path.endswith('.hkp'):
590 self.do_loadlist(file_path)
593 def _on_erase_background(self, event):
596 def _on_notebook_page_close(self, event):
597 ctrl = event.GetEventObject()
598 playlist_name = ctrl.GetPageText(ctrl._curpage)
599 self.DeleteFromPlaylists(playlist_name)
601 def OnPaneClose(self, event):
604 def OnPropGridChanged (self, event):
605 prop = event.GetProperty()
607 item_section = self.panelProperties.SelectedTreeItem
608 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
609 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
610 config = self.gui.config[plugin]
611 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
612 property_key = prop.GetName()
613 property_value = prop.GetDisplayedString()
615 config[property_section][property_key]['value'] = property_value
617 def OnResultsCheck(self, index, flag):
618 results = self.GetActivePlot().results
619 if results.has_key(self.results_str):
620 results[self.results_str].results[index].visible = flag
621 results[self.results_str].update()
625 def _on_size(self, event):
628 def UpdatePlaylistsTreeSelection(self):
629 playlist = self.GetActivePlaylist()
630 if playlist is not None:
631 if playlist.index >= 0:
632 self._c['status bar'].set_playlist(playlist)
636 def _on_curve_select(self, playlist, curve):
637 #create the plot tab and add playlist to the dictionary
638 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
639 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
640 #tab_index = self._c['notebook'].GetSelection()
641 playlist.figure = plotPanel.get_figure()
642 self.playlists[playlist.name] = playlist
643 #self.playlists[playlist.name] = [playlist, figure]
644 self._c['status bar'].set_playlist(playlist)
649 def _on_playlist_left_doubleclick(self):
650 index = self._c['notebook'].GetSelection()
651 current_playlist = self._c['notebook'].GetPageText(index)
652 if current_playlist != playlist_name:
653 index = self._GetPlaylistTab(playlist_name)
654 self._c['notebook'].SetSelection(index)
655 self._c['status bar'].set_playlist(playlist)
659 def _on_playlist_delete(self, playlist):
660 notebook = self.Parent.plotNotebook
661 index = self.Parent._GetPlaylistTab(playlist.name)
662 notebook.SetSelection(index)
663 notebook.DeletePage(notebook.GetSelection())
664 self.Parent.DeleteFromPlaylists(playlist_name)
668 # Command panel interface
670 def select_command(self, _class, method, command):
671 #self.select_plugin(plugin=command.plugin)
672 if 'assistant' in self._c:
673 self._c['assitant'].ChangeValue(command.help)
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 os.remove(curve.path)
735 def _on_set_selected_playlist(self, _class, method, playlist):
736 """Call the `jump to playlist` command.
738 results = self.execute_command(
739 command=self._command_by_name('playlists'))
740 if not isinstance(results[-1], Success):
742 assert len(results) == 2, results
743 playlists = results[0]
744 matching = [p for p in playlists if p.name == playlist.name]
745 assert len(matching) == 1, matching
746 index = playlists.index(matching[0])
747 results = self.execute_command(
748 command=self._command_by_name('jump to playlist'),
749 args={'index':index})
751 def _on_set_selected_curve(self, _class, method, playlist, curve):
752 """Call the `jump to curve` command.
754 self._on_set_selected_playlist(_class, method, playlist)
755 index = playlist.index(curve)
756 results = self.execute_command(
757 command=self._command_by_name('jump to curve'),
758 args={'index':index})
759 if not isinstance(results[-1], Success):
761 #results = self.execute_command(
762 # command=self._command_by_name('get playlist'))
763 #if not isinstance(results[-1], Success):
765 self.execute_command(
766 command=self._command_by_name('get curve'))
772 def _next_curve(self, *args):
773 """Call the `next curve` command.
775 results = self.execute_command(
776 command=self._command_by_name('next curve'))
777 if isinstance(results[-1], Success):
778 self.execute_command(
779 command=self._command_by_name('get curve'))
781 def _previous_curve(self, *args):
782 """Call the `previous curve` command.
784 results = self.execute_command(
785 command=self._command_by_name('previous curve'))
786 if isinstance(results[-1], Success):
787 self.execute_command(
788 command=self._command_by_name('get curve'))
792 # Panel display handling
794 def _on_panel_visibility(self, _class, method, panel_name, visible):
795 pane = self._c['manager'].GetPane(panel_name)
797 #if we don't do the following, the Folders pane does not resize properly on hide/show
798 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
799 #folders_size = pane.GetSize()
800 self.panelFolders.Fit()
801 self._c['manager'].Update()
803 def _setup_perspectives(self):
804 """Add perspectives to menubar and _perspectives.
806 self._perspectives = {
807 'Default': self._c['manager'].SavePerspective(),
809 path = self.gui.config['perspective path']
810 if os.path.isdir(path):
811 files = sorted(os.listdir(path))
813 name, extension = os.path.splitext(fname)
814 if extension != self.gui.config['perspective extension']:
816 fpath = os.path.join(path, fname)
817 if not os.path.isfile(fpath):
820 with open(fpath, 'rU') as f:
821 perspective = f.readline()
823 self._perspectives[name] = perspective
825 selected_perspective = self.gui.config['active perspective']
826 if not self._perspectives.has_key(selected_perspective):
827 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
829 self._restore_perspective(selected_perspective, force=True)
830 self._update_perspective_menu()
832 def _update_perspective_menu(self):
833 self._c['menu bar']._c['perspective'].update(
834 sorted(self._perspectives.keys()),
835 self.gui.config['active perspective'])
837 def _save_perspective(self, perspective, perspective_dir, name,
839 path = os.path.join(perspective_dir, name)
840 if extension != None:
842 if not os.path.isdir(perspective_dir):
843 os.makedirs(perspective_dir)
844 with open(path, 'w') as f:
846 self._perspectives[name] = perspective
847 self._restore_perspective(name)
848 self._update_perspective_menu()
850 def _delete_perspectives(self, perspective_dir, names,
852 self.log.debug('remove perspectives %s from %s'
853 % (names, perspective_dir))
855 path = os.path.join(perspective_dir, name)
856 if extension != None:
859 del(self._perspectives[name])
860 self._update_perspective_menu()
861 if self.gui.config['active perspective'] in names:
862 self._restore_perspective('Default')
863 # TODO: does this bug still apply?
864 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
865 # http://trac.wxwidgets.org/ticket/3258
866 # ) that makes the radio item indicator in the menu disappear.
867 # The code should be fine once this issue is fixed.
869 def _restore_perspective(self, name, force=False):
870 if name != self.gui.config['active perspective'] or force == True:
871 self.log.debug('restore perspective %s' % name)
872 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
873 self._c['manager'].LoadPerspective(self._perspectives[name])
874 self._c['manager'].Update()
875 for pane in self._c['manager'].GetAllPanes():
876 view = self._c['menu bar']._c['view']
877 if pane.name in view._c.keys():
878 view._c[pane.name].Check(pane.window.IsShown())
880 def _on_save_perspective(self, *args):
881 perspective = self._c['manager'].SavePerspective()
882 name = self.gui.config['active perspective']
883 if name == 'Default':
884 name = 'New perspective'
885 name = select_save_file(
886 directory=self.gui.config['perspective path'],
888 extension=self.gui.config['perspective extension'],
890 message='Enter a name for the new perspective:',
891 caption='Save perspective')
894 self._save_perspective(
895 perspective, self.gui.config['perspective path'], name=name,
896 extension=self.gui.config['perspective extension'])
898 def _on_delete_perspective(self, *args, **kwargs):
899 options = sorted([p for p in self._perspectives.keys()
901 dialog = SelectionDialog(
903 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
904 button_id=wx.ID_DELETE,
905 selection_style='multiple',
907 title='Delete perspective(s)',
908 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
909 dialog.CenterOnScreen()
911 names = [options[i] for i in dialog.selected]
913 self._delete_perspectives(
914 self.gui.config['perspective path'], names=names,
915 extension=self.gui.config['perspective extension'])
917 def _on_select_perspective(self, _class, method, name):
918 self._restore_perspective(name)
922 class HookeApp (wx.App):
923 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
925 Tosses up a splash screen and then loads :class:`HookeFrame` in
928 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
930 self.commands = commands
931 self.inqueue = inqueue
932 self.outqueue = outqueue
933 super(HookeApp, self).__init__(*args, **kwargs)
936 self.SetAppName('Hooke')
937 self.SetVendorName('')
938 self._setup_splash_screen()
940 height = self.gui.config['main height']
941 width = self.gui.config['main width']
942 top = self.gui.config['main top']
943 left = self.gui.config['main left']
945 # Sometimes, the ini file gets confused and sets 'left' and
946 # 'top' to large negative numbers. Here we catch and fix
947 # this. Keep small negative numbers, the user might want
956 self.gui, self.commands, self.inqueue, self.outqueue,
957 parent=None, title='Hooke',
958 pos=(left, top), size=(width, height),
959 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
961 self._c['frame'].Show(True)
962 self.SetTopWindow(self._c['frame'])
965 def _setup_splash_screen(self):
966 if self.gui.config['show splash screen'] == True:
967 path = self.gui.config['splash screen image']
968 if os.path.isfile(path):
969 duration = self.gui.config['splash screen duration']
971 bitmap=wx.Image(path).ConvertToBitmap(),
972 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
973 milliseconds=duration,
976 # For some reason splashDuration and sleep do not
977 # correspond to each other at least not on Windows.
978 # Maybe it's because duration is in milliseconds and
979 # sleep in seconds. Thus we need to increase the
980 # sleep time a bit. A factor of 1.2 seems to work.
982 time.sleep(sleepFactor * duration / 1000)
985 class GUI (UserInterface):
986 """wxWindows graphical user interface.
989 super(GUI, self).__init__(name='gui')
991 def default_settings(self):
992 """Return a list of :class:`hooke.config.Setting`\s for any
993 configurable UI settings.
995 The suggested section setting is::
997 Setting(section=self.setting_section, help=self.__doc__)
1000 Setting(section=self.setting_section, help=self.__doc__),
1001 Setting(section=self.setting_section, option='icon image',
1002 value=os.path.join('doc', 'img', 'microscope.ico'),
1004 help='Path to the hooke icon image.'),
1005 Setting(section=self.setting_section, option='show splash screen',
1006 value=True, type='bool',
1007 help='Enable/disable the splash screen'),
1008 Setting(section=self.setting_section, option='splash screen image',
1009 value=os.path.join('doc', 'img', 'hooke.jpg'),
1011 help='Path to the Hooke splash screen image.'),
1012 Setting(section=self.setting_section,
1013 option='splash screen duration',
1014 value=1000, type='int',
1015 help='Duration of the splash screen in milliseconds.'),
1016 Setting(section=self.setting_section, option='perspective path',
1017 value=os.path.join('resources', 'gui', 'perspective'),
1018 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1019 Setting(section=self.setting_section, option='perspective extension',
1021 help='Extension for perspective files.'),
1022 Setting(section=self.setting_section, option='hide extensions',
1023 value=False, type='bool',
1024 help='Hide file extensions when displaying names.'),
1025 Setting(section=self.setting_section, option='plot legend',
1026 value=True, type='bool',
1027 help='Enable/disable the plot legend.'),
1028 Setting(section=self.setting_section, option='plot SI format',
1029 value='True', type='bool',
1030 help='Enable/disable SI plot axes numbering.'),
1031 Setting(section=self.setting_section, option='plot decimals',
1032 value=2, type='int',
1033 help='Number of decimal places to show if "plot SI format" is enabled.'),
1034 Setting(section=self.setting_section, option='folders-workdir',
1035 value='.', type='path',
1036 help='This should probably go...'),
1037 Setting(section=self.setting_section, option='folders-filters',
1038 value='.', type='path',
1039 help='This should probably go...'),
1040 Setting(section=self.setting_section, option='active perspective',
1042 help='Name of active perspective file (or "Default").'),
1043 Setting(section=self.setting_section,
1044 option='folders-filter-index',
1045 value=0, type='int',
1046 help='This should probably go...'),
1047 Setting(section=self.setting_section, option='main height',
1048 value=450, type='int',
1049 help='Height of main window in pixels.'),
1050 Setting(section=self.setting_section, option='main width',
1051 value=800, type='int',
1052 help='Width of main window in pixels.'),
1053 Setting(section=self.setting_section, option='main top',
1054 value=0, type='int',
1055 help='Pixels from screen top to top of main window.'),
1056 Setting(section=self.setting_section, option='main left',
1057 value=0, type='int',
1058 help='Pixels from screen left to left of main window.'),
1059 Setting(section=self.setting_section, option='selected command',
1060 value='load playlist',
1061 help='Name of the initially selected command.'),
1064 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1068 app = HookeApp(gui=self,
1070 inqueue=ui_to_command_queue,
1071 outqueue=command_to_ui_queue,
1075 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1076 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)