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)]
342 while (len(args[arg.name]) > 0
343 and args[arg.name][-1] == None):
345 if len(args[arg.name]) == 0:
346 args[arg.name] = arg.default
347 self.log.debug('executing %s with %s' % (command.name, args))
348 self.inqueue.put(CommandMessage(command, args))
351 msg = self.outqueue.get()
353 if isinstance(msg, Exit):
356 elif isinstance(msg, CommandExit):
357 # TODO: display command complete
359 elif isinstance(msg, ReloadUserInterfaceConfig):
360 self.gui.reload_config(msg.config)
362 elif isinstance(msg, Request):
363 h = handler.HANDLERS[msg.type]
364 h.run(self, msg) # TODO: pause for response?
367 self, '_postprocess_%s' % command.name.replace(' ', '_'),
368 self._postprocess_text)
369 pp(command=command, args=args, results=results)
372 def _handle_request(self, msg):
373 """Repeatedly try to get a response to `msg`.
376 raise NotImplementedError('_%s_request_prompt' % msg.type)
377 prompt_string = prompt(msg)
378 parser = getattr(self, '_%s_request_parser' % msg.type, None)
380 raise NotImplementedError('_%s_request_parser' % msg.type)
384 self.cmd.stdout.write(''.join([
385 error.__class__.__name__, ': ', str(error), '\n']))
386 self.cmd.stdout.write(prompt_string)
387 value = parser(msg, self.cmd.stdin.readline())
389 response = msg.response(value)
391 except ValueError, error:
393 self.inqueue.put(response)
397 # Command-specific postprocessing
399 def _postprocess_text(self, command, args={}, results=[]):
400 """Print the string representation of the results to the Results window.
402 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
403 approach, except that :class:`~hooke.ui.commandline.DoCommand`
404 doesn't print some internally handled messages
405 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
407 for result in results:
408 if isinstance(result, CommandExit):
409 self._c['output'].write(result.__class__.__name__+'\n')
410 self._c['output'].write(str(result).rstrip()+'\n')
412 def _postprocess_load_playlist(self, command, args={}, results=None):
413 """Update `self` to show the playlist.
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'].add_playlist(playlist)
422 def _postprocess_get_playlist(self, command, args={}, results=[]):
423 if not isinstance(results[-1], Success):
424 self._postprocess_text(command, results=results)
426 assert len(results) == 2, results
427 playlist = results[0]
428 self._c['playlist']._c['tree'].update_playlist(playlist)
430 def _postprocess_get_curve(self, command, args={}, results=[]):
431 """Update `self` to show the curve.
433 if not isinstance(results[-1], Success):
434 self._postprocess_text(command, results=results)
436 assert len(results) == 2, results
438 if args.get('curve', None) == None:
439 # the command defaults to the current curve of the current playlist
440 results = self.execute_command(
441 command=self._command_by_name('get playlist'))
442 playlist = results[0]
444 raise NotImplementedError()
445 if 'note' in self._c:
446 self._c['note'].set_text(curve.info['note'])
447 if 'playlist' in self._c:
448 self._c['playlist']._c['tree'].set_selected_curve(
450 if 'plot' in self._c:
451 self._c['plot'].set_curve(curve, config=self.gui.config)
453 def _postprocess_next_curve(self, command, args={}, results=[]):
454 """No-op. Only call 'next curve' via `self._next_curve()`.
458 def _postprocess_previous_curve(self, command, args={}, results=[]):
459 """No-op. Only call 'previous curve' via `self._previous_curve()`.
463 def _postprocess_zero_block_surface_contact_point(
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'))
471 def _postprocess_add_block_force_array(
472 self, command, args={}, results=[]):
473 """Update the curve, since the available columns may have changed.
475 if isinstance(results[-1], Success):
476 self.execute_command(
477 command=self._command_by_name('get curve'))
483 def _GetActiveFileIndex(self):
484 lib.playlist.Playlist = self.GetActivePlaylist()
485 #get the selected item from the tree
486 selected_item = self._c['playlist']._c['tree'].GetSelection()
487 #test if a playlist or a curve was double-clicked
488 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
492 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
493 while selected_item.IsOk():
495 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
498 def _GetPlaylistTab(self, name):
499 for index, page in enumerate(self._c['notebook']._tabs._pages):
500 if page.caption == name:
504 def select_plugin(self, _class=None, method=None, plugin=None):
507 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
509 playlist = lib.playlist.Playlist(self, self.drivers)
511 playlist.add_curve(item)
512 if playlist.count > 0:
513 playlist.name = self._GetUniquePlaylistName(name)
515 self.AddTayliss(playlist)
517 def AppliesPlotmanipulator(self, name):
519 Returns True if the plotmanipulator 'name' is applied, False otherwise
520 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
522 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
524 def ApplyPlotmanipulators(self, plot, plot_file):
526 Apply all active plotmanipulators.
528 if plot is not None and plot_file is not None:
529 manipulated_plot = copy.deepcopy(plot)
530 for plotmanipulator in self.plotmanipulators:
531 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
532 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
533 return manipulated_plot
535 def GetActiveFigure(self):
536 playlist_name = self.GetActivePlaylistName()
537 figure = self.playlists[playlist_name].figure
538 if figure is not None:
542 def GetActiveFile(self):
543 playlist = self.GetActivePlaylist()
544 if playlist is not None:
545 return playlist.get_active_file()
548 def GetActivePlot(self):
549 playlist = self.GetActivePlaylist()
550 if playlist is not None:
551 return playlist.get_active_file().plot
554 def GetDisplayedPlot(self):
555 plot = copy.deepcopy(self.displayed_plot)
557 #plot.curves = copy.deepcopy(plot.curves)
560 def GetDisplayedPlotCorrected(self):
561 plot = copy.deepcopy(self.displayed_plot)
563 plot.curves = copy.deepcopy(plot.corrected_curves)
566 def GetDisplayedPlotRaw(self):
567 plot = copy.deepcopy(self.displayed_plot)
569 plot.curves = copy.deepcopy(plot.raw_curves)
572 def GetDockArt(self):
573 return self._c['manager'].GetArtProvider()
575 def GetPlotmanipulator(self, name):
577 Returns a plot manipulator function from its name
579 for plotmanipulator in self.plotmanipulators:
580 if plotmanipulator.name == name:
581 return plotmanipulator
584 def HasPlotmanipulator(self, name):
586 returns True if the plotmanipulator 'name' is loaded, False otherwise
588 for plotmanipulator in self.plotmanipulators:
589 if plotmanipulator.command == name:
594 def _on_dir_ctrl_left_double_click(self, event):
595 file_path = self.panelFolders.GetPath()
596 if os.path.isfile(file_path):
597 if file_path.endswith('.hkp'):
598 self.do_loadlist(file_path)
601 def _on_erase_background(self, event):
604 def _on_notebook_page_close(self, event):
605 ctrl = event.GetEventObject()
606 playlist_name = ctrl.GetPageText(ctrl._curpage)
607 self.DeleteFromPlaylists(playlist_name)
609 def OnPaneClose(self, event):
612 def OnPropGridChanged (self, event):
613 prop = event.GetProperty()
615 item_section = self.panelProperties.SelectedTreeItem
616 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
617 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
618 config = self.gui.config[plugin]
619 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
620 property_key = prop.GetName()
621 property_value = prop.GetDisplayedString()
623 config[property_section][property_key]['value'] = property_value
625 def OnResultsCheck(self, index, flag):
626 results = self.GetActivePlot().results
627 if results.has_key(self.results_str):
628 results[self.results_str].results[index].visible = flag
629 results[self.results_str].update()
633 def _on_size(self, event):
636 def UpdatePlaylistsTreeSelection(self):
637 playlist = self.GetActivePlaylist()
638 if playlist is not None:
639 if playlist.index >= 0:
640 self._c['status bar'].set_playlist(playlist)
644 def _on_curve_select(self, playlist, curve):
645 #create the plot tab and add playlist to the dictionary
646 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
647 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
648 #tab_index = self._c['notebook'].GetSelection()
649 playlist.figure = plotPanel.get_figure()
650 self.playlists[playlist.name] = playlist
651 #self.playlists[playlist.name] = [playlist, figure]
652 self._c['status bar'].set_playlist(playlist)
657 def _on_playlist_left_doubleclick(self):
658 index = self._c['notebook'].GetSelection()
659 current_playlist = self._c['notebook'].GetPageText(index)
660 if current_playlist != playlist_name:
661 index = self._GetPlaylistTab(playlist_name)
662 self._c['notebook'].SetSelection(index)
663 self._c['status bar'].set_playlist(playlist)
667 def _on_playlist_delete(self, playlist):
668 notebook = self.Parent.plotNotebook
669 index = self.Parent._GetPlaylistTab(playlist.name)
670 notebook.SetSelection(index)
671 notebook.DeletePage(notebook.GetSelection())
672 self.Parent.DeleteFromPlaylists(playlist_name)
676 # Command panel interface
678 def select_command(self, _class, method, command):
679 #self.select_plugin(plugin=command.plugin)
680 self._c['property editor'].clear()
681 self._c['property editor']._argument_from_label = {}
682 for argument in command.arguments:
683 if argument.name == 'help':
686 results = self.execute_command(
687 command=self._command_by_name('playlists'))
688 if not isinstance(results[-1], Success):
689 self._postprocess_text(command, results=results)
692 playlists = results[0]
694 results = self.execute_command(
695 command=self._command_by_name('playlist curves'))
696 if not isinstance(results[-1], Success):
697 self._postprocess_text(command, results=results)
702 ret = props_from_argument(
703 argument, curves=curves, playlists=playlists)
705 continue # property intentionally not handled (yet)
707 self._c['property editor'].append_property(p)
708 self._c['property editor']._argument_from_label[label] = (
711 self.gui.config['selected command'] = command # TODO: push to engine
715 # Note panel interface
717 def _on_update_note(self, _class, method, text):
718 """Sets the note for the active curve.
720 self.execute_command(
721 command=self._command_by_name('set note'),
726 # Playlist panel interface
728 def _on_user_delete_playlist(self, _class, method, playlist):
731 def _on_delete_playlist(self, _class, method, playlist):
732 if hasattr(playlist, 'path') and playlist.path != None:
733 os.remove(playlist.path)
735 def _on_user_delete_curve(self, _class, method, playlist, curve):
738 def _on_delete_curve(self, _class, method, playlist, curve):
739 # TODO: execute_command 'remove curve from playlist'
740 os.remove(curve.path)
742 def _on_set_selected_playlist(self, _class, method, playlist):
743 """Call the `jump to playlist` command.
745 results = self.execute_command(
746 command=self._command_by_name('playlists'))
747 if not isinstance(results[-1], Success):
749 assert len(results) == 2, results
750 playlists = results[0]
751 matching = [p for p in playlists if p.name == playlist.name]
752 assert len(matching) == 1, matching
753 index = playlists.index(matching[0])
754 results = self.execute_command(
755 command=self._command_by_name('jump to playlist'),
756 args={'index':index})
758 def _on_set_selected_curve(self, _class, method, playlist, curve):
759 """Call the `jump to curve` command.
761 self._on_set_selected_playlist(_class, method, playlist)
762 index = playlist.index(curve)
763 results = self.execute_command(
764 command=self._command_by_name('jump to curve'),
765 args={'index':index})
766 if not isinstance(results[-1], Success):
768 #results = self.execute_command(
769 # command=self._command_by_name('get playlist'))
770 #if not isinstance(results[-1], Success):
772 self.execute_command(
773 command=self._command_by_name('get curve'))
777 # Plot panel interface
779 def _on_plot_status_text(self, _class, method, text):
780 if 'status bar' in self._c:
781 self._c['status bar'].set_plot_text(text)
787 def _next_curve(self, *args):
788 """Call the `next curve` command.
790 results = self.execute_command(
791 command=self._command_by_name('next curve'))
792 if isinstance(results[-1], Success):
793 self.execute_command(
794 command=self._command_by_name('get curve'))
796 def _previous_curve(self, *args):
797 """Call the `previous curve` command.
799 results = self.execute_command(
800 command=self._command_by_name('previous curve'))
801 if isinstance(results[-1], Success):
802 self.execute_command(
803 command=self._command_by_name('get curve'))
807 # Panel display handling
809 def _on_panel_visibility(self, _class, method, panel_name, visible):
810 pane = self._c['manager'].GetPane(panel_name)
812 #if we don't do the following, the Folders pane does not resize properly on hide/show
813 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
814 #folders_size = pane.GetSize()
815 self.panelFolders.Fit()
816 self._c['manager'].Update()
818 def _setup_perspectives(self):
819 """Add perspectives to menubar and _perspectives.
821 self._perspectives = {
822 'Default': self._c['manager'].SavePerspective(),
824 path = self.gui.config['perspective path']
825 if os.path.isdir(path):
826 files = sorted(os.listdir(path))
828 name, extension = os.path.splitext(fname)
829 if extension != self.gui.config['perspective extension']:
831 fpath = os.path.join(path, fname)
832 if not os.path.isfile(fpath):
835 with open(fpath, 'rU') as f:
836 perspective = f.readline()
838 self._perspectives[name] = perspective
840 selected_perspective = self.gui.config['active perspective']
841 if not self._perspectives.has_key(selected_perspective):
842 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
844 self._restore_perspective(selected_perspective, force=True)
845 self._update_perspective_menu()
847 def _update_perspective_menu(self):
848 self._c['menu bar']._c['perspective'].update(
849 sorted(self._perspectives.keys()),
850 self.gui.config['active perspective'])
852 def _save_perspective(self, perspective, perspective_dir, name,
854 path = os.path.join(perspective_dir, name)
855 if extension != None:
857 if not os.path.isdir(perspective_dir):
858 os.makedirs(perspective_dir)
859 with open(path, 'w') as f:
861 self._perspectives[name] = perspective
862 self._restore_perspective(name)
863 self._update_perspective_menu()
865 def _delete_perspectives(self, perspective_dir, names,
867 self.log.debug('remove perspectives %s from %s'
868 % (names, perspective_dir))
870 path = os.path.join(perspective_dir, name)
871 if extension != None:
874 del(self._perspectives[name])
875 self._update_perspective_menu()
876 if self.gui.config['active perspective'] in names:
877 self._restore_perspective('Default')
878 # TODO: does this bug still apply?
879 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
880 # http://trac.wxwidgets.org/ticket/3258
881 # ) that makes the radio item indicator in the menu disappear.
882 # The code should be fine once this issue is fixed.
884 def _restore_perspective(self, name, force=False):
885 if name != self.gui.config['active perspective'] or force == True:
886 self.log.debug('restore perspective %s' % name)
887 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
888 self._c['manager'].LoadPerspective(self._perspectives[name])
889 self._c['manager'].Update()
890 for pane in self._c['manager'].GetAllPanes():
891 view = self._c['menu bar']._c['view']
892 if pane.name in view._c.keys():
893 view._c[pane.name].Check(pane.window.IsShown())
895 def _on_save_perspective(self, *args):
896 perspective = self._c['manager'].SavePerspective()
897 name = self.gui.config['active perspective']
898 if name == 'Default':
899 name = 'New perspective'
900 name = select_save_file(
901 directory=self.gui.config['perspective path'],
903 extension=self.gui.config['perspective extension'],
905 message='Enter a name for the new perspective:',
906 caption='Save perspective')
909 self._save_perspective(
910 perspective, self.gui.config['perspective path'], name=name,
911 extension=self.gui.config['perspective extension'])
913 def _on_delete_perspective(self, *args, **kwargs):
914 options = sorted([p for p in self._perspectives.keys()
916 dialog = SelectionDialog(
918 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
919 button_id=wx.ID_DELETE,
920 selection_style='multiple',
922 title='Delete perspective(s)',
923 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
924 dialog.CenterOnScreen()
926 if dialog.canceled == True:
928 names = [options[i] for i in dialog.selected]
930 self._delete_perspectives(
931 self.gui.config['perspective path'], names=names,
932 extension=self.gui.config['perspective extension'])
934 def _on_select_perspective(self, _class, method, name):
935 self._restore_perspective(name)
939 class HookeApp (wx.App):
940 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
942 Tosses up a splash screen and then loads :class:`HookeFrame` in
945 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
947 self.commands = commands
948 self.inqueue = inqueue
949 self.outqueue = outqueue
950 super(HookeApp, self).__init__(*args, **kwargs)
953 self.SetAppName('Hooke')
954 self.SetVendorName('')
955 self._setup_splash_screen()
957 height = self.gui.config['main height']
958 width = self.gui.config['main width']
959 top = self.gui.config['main top']
960 left = self.gui.config['main left']
962 # Sometimes, the ini file gets confused and sets 'left' and
963 # 'top' to large negative numbers. Here we catch and fix
964 # this. Keep small negative numbers, the user might want
973 self.gui, self.commands, self.inqueue, self.outqueue,
974 parent=None, title='Hooke',
975 pos=(left, top), size=(width, height),
976 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
978 self._c['frame'].Show(True)
979 self.SetTopWindow(self._c['frame'])
982 def _setup_splash_screen(self):
983 if self.gui.config['show splash screen'] == True:
984 path = self.gui.config['splash screen image']
985 if os.path.isfile(path):
986 duration = self.gui.config['splash screen duration']
988 bitmap=wx.Image(path).ConvertToBitmap(),
989 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
990 milliseconds=duration,
993 # For some reason splashDuration and sleep do not
994 # correspond to each other at least not on Windows.
995 # Maybe it's because duration is in milliseconds and
996 # sleep in seconds. Thus we need to increase the
997 # sleep time a bit. A factor of 1.2 seems to work.
999 time.sleep(sleepFactor * duration / 1000)
1002 class GUI (UserInterface):
1003 """wxWindows graphical user interface.
1006 super(GUI, self).__init__(name='gui')
1008 def default_settings(self):
1009 """Return a list of :class:`hooke.config.Setting`\s for any
1010 configurable UI settings.
1012 The suggested section setting is::
1014 Setting(section=self.setting_section, help=self.__doc__)
1017 Setting(section=self.setting_section, help=self.__doc__),
1018 Setting(section=self.setting_section, option='icon image',
1019 value=os.path.join('doc', 'img', 'microscope.ico'),
1021 help='Path to the hooke icon image.'),
1022 Setting(section=self.setting_section, option='show splash screen',
1023 value=True, type='bool',
1024 help='Enable/disable the splash screen'),
1025 Setting(section=self.setting_section, option='splash screen image',
1026 value=os.path.join('doc', 'img', 'hooke.jpg'),
1028 help='Path to the Hooke splash screen image.'),
1029 Setting(section=self.setting_section,
1030 option='splash screen duration',
1031 value=1000, type='int',
1032 help='Duration of the splash screen in milliseconds.'),
1033 Setting(section=self.setting_section, option='perspective path',
1034 value=os.path.join('resources', 'gui', 'perspective'),
1035 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1036 Setting(section=self.setting_section, option='perspective extension',
1038 help='Extension for perspective files.'),
1039 Setting(section=self.setting_section, option='hide extensions',
1040 value=False, type='bool',
1041 help='Hide file extensions when displaying names.'),
1042 Setting(section=self.setting_section, option='plot legend',
1043 value=True, type='bool',
1044 help='Enable/disable the plot legend.'),
1045 Setting(section=self.setting_section, option='plot SI format',
1046 value='True', type='bool',
1047 help='Enable/disable SI plot axes numbering.'),
1048 Setting(section=self.setting_section, option='plot decimals',
1049 value=2, type='int',
1050 help='Number of decimal places to show if "plot SI format" is enabled.'),
1051 Setting(section=self.setting_section, option='folders-workdir',
1052 value='.', type='path',
1053 help='This should probably go...'),
1054 Setting(section=self.setting_section, option='folders-filters',
1055 value='.', type='path',
1056 help='This should probably go...'),
1057 Setting(section=self.setting_section, option='active perspective',
1059 help='Name of active perspective file (or "Default").'),
1060 Setting(section=self.setting_section,
1061 option='folders-filter-index',
1062 value=0, type='int',
1063 help='This should probably go...'),
1064 Setting(section=self.setting_section, option='main height',
1065 value=450, type='int',
1066 help='Height of main window in pixels.'),
1067 Setting(section=self.setting_section, option='main width',
1068 value=800, type='int',
1069 help='Width of main window in pixels.'),
1070 Setting(section=self.setting_section, option='main top',
1071 value=0, type='int',
1072 help='Pixels from screen top to top of main window.'),
1073 Setting(section=self.setting_section, option='main left',
1074 value=0, type='int',
1075 help='Pixels from screen left to left of main window.'),
1076 Setting(section=self.setting_section, option='selected command',
1077 value='load playlist',
1078 help='Name of the initially selected command.'),
1081 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1085 app = HookeApp(gui=self,
1087 inqueue=ui_to_command_queue,
1088 outqueue=command_to_ui_queue,
1092 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1093 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)