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':[400, 1000]},
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 # ('assistant', wx.TextCtrl(
199 # pos=wx.Point(0, 0),
200 # size=wx.Size(150, 90),
201 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
202 (panel.PANELS['plot'](
206 style=wx.WANTS_CHARS|wx.NO_BORDER,
207 # WANTS_CHARS so the panel doesn't eat the Return key.
210 (panel.PANELS['output'](
213 size=wx.Size(150, 90),
214 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
216 # ('results', panel.results.Results(self), 'bottom'),
218 self._add_panel(p, style)
219 #self._c['assistant'].SetEditable(False)
221 def _add_panel(self, panel, style):
222 self._c[panel.name] = panel
223 m_name = panel.managed_name
224 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
225 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
228 elif style == 'center':
230 elif style == 'left':
232 elif style == 'right':
235 assert style == 'bottom', style
237 self._c['manager'].AddPane(panel, info)
239 def _setup_toolbars(self):
240 self._c['navigation bar'] = navbar.NavBar(
242 'next': self._next_curve,
243 'previous': self._previous_curve,
246 style=wx.TB_FLAT | wx.TB_NODIVIDER)
247 self._c['manager'].AddPane(
248 self._c['navigation bar'],
249 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
250 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
251 ).RightDockable(False))
253 def _bind_events(self):
254 # TODO: figure out if we can use the eventManager for menu
255 # ranges and events of 'self' without raising an assertion
257 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
258 self.Bind(wx.EVT_SIZE, self._on_size)
259 self.Bind(wx.EVT_CLOSE, self._on_close)
260 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
261 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
263 return # TODO: cleanup
264 treeCtrl = self._c['folders'].GetTreeCtrl()
265 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
268 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
270 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
272 def _on_about(self, *args):
273 dialog = wx.MessageDialog(
275 message=self.gui._splash_text(extra_info={
276 'get-details':'click "Help -> License"'},
278 caption='About Hooke',
279 style=wx.OK|wx.ICON_INFORMATION)
283 def _on_close(self, *args):
284 self.log.info('closing GUI framework')
286 self.gui.config['main height'] = str(self.GetSize().GetHeight())
287 self.gui.config['main left'] = str(self.GetPosition()[0])
288 self.gui.config['main top'] = str(self.GetPosition()[1])
289 self.gui.config['main width'] = str(self.GetSize().GetWidth())
290 # push changes back to Hooke.config?
291 self._c['manager'].UnInit()
292 del self._c['manager']
297 # Panel utility functions
299 def _file_name(self, name):
300 """Cleanup names according to configured preferences.
302 if self.gui.config['hide extensions'] == True:
303 name,ext = os.path.splitext(name)
310 def _command_by_name(self, name):
311 cs = [c for c in self.commands if c.name == name]
315 raise Exception('Multiple commands named "%s"' % name)
318 def execute_command(self, _class=None, method=None,
319 command=None, args=None):
322 if ('property editor' in self._c
323 and self.gui.config['selected command'] == command):
324 for name,value in self._c['property editor'].get_values().items():
325 arg = self._c['property editor']._argument_from_label.get(
330 args[arg.name] = value
332 # deal with counted arguments
333 if arg.name not in args:
335 index = int(name[len(arg.name):])
336 args[arg.name][index] = value
337 for arg in command.arguments:
338 if arg.count != 1 and arg.name in args:
339 keys = sorted(args[arg.name].keys())
340 assert keys == range(arg.count), keys
341 args[arg.name] = [args[arg.name][i]
342 for i in range(arg.count)]
343 self.log.debug('executing %s with %s' % (command.name, args))
344 self.inqueue.put(CommandMessage(command, args))
347 msg = self.outqueue.get()
349 if isinstance(msg, Exit):
352 elif isinstance(msg, CommandExit):
353 # TODO: display command complete
355 elif isinstance(msg, ReloadUserInterfaceConfig):
356 self.gui.reload_config(msg.config)
358 elif isinstance(msg, Request):
359 h = handler.HANDLERS[msg.type]
360 h.run(self, msg) # TODO: pause for response?
363 self, '_postprocess_%s' % command.name.replace(' ', '_'),
364 self._postprocess_text)
365 pp(command=command, args=args, results=results)
368 def _handle_request(self, msg):
369 """Repeatedly try to get a response to `msg`.
372 raise NotImplementedError('_%s_request_prompt' % msg.type)
373 prompt_string = prompt(msg)
374 parser = getattr(self, '_%s_request_parser' % msg.type, None)
376 raise NotImplementedError('_%s_request_parser' % msg.type)
380 self.cmd.stdout.write(''.join([
381 error.__class__.__name__, ': ', str(error), '\n']))
382 self.cmd.stdout.write(prompt_string)
383 value = parser(msg, self.cmd.stdin.readline())
385 response = msg.response(value)
387 except ValueError, error:
389 self.inqueue.put(response)
393 # Command-specific postprocessing
395 def _postprocess_text(self, command, args={}, results=[]):
396 """Print the string representation of the results to the Results window.
398 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
399 approach, except that :class:`~hooke.ui.commandline.DoCommand`
400 doesn't print some internally handled messages
401 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
403 for result in results:
404 if isinstance(result, CommandExit):
405 self._c['output'].write(result.__class__.__name__+'\n')
406 self._c['output'].write(str(result).rstrip()+'\n')
408 def _postprocess_load_playlist(self, command, args={}, results=None):
409 """Update `self` to show the playlist.
411 if not isinstance(results[-1], Success):
412 self._postprocess_text(command, results=results)
414 assert len(results) == 2, results
415 playlist = results[0]
416 self._c['playlist']._c['tree'].add_playlist(playlist)
418 def _postprocess_get_playlist(self, command, args={}, results=[]):
419 if not isinstance(results[-1], Success):
420 self._postprocess_text(command, results=results)
422 assert len(results) == 2, results
423 playlist = results[0]
424 self._c['playlist']._c['tree'].update_playlist(playlist)
426 def _postprocess_get_curve(self, command, args={}, results=[]):
427 """Update `self` to show the curve.
429 if not isinstance(results[-1], Success):
430 self._postprocess_text(command, results=results)
432 assert len(results) == 2, results
434 if args.get('curve', None) == None:
435 # the command defaults to the current curve of the current playlist
436 results = self.execute_command(
437 command=self._command_by_name('get playlist'))
438 playlist = results[0]
440 raise NotImplementedError()
441 if 'note' in self._c:
442 self._c['note'].set_text(curve.info['note'])
443 if 'playlist' in self._c:
444 self._c['playlist']._c['tree'].set_selected_curve(
446 if 'plot' in self._c:
447 self._c['plot'].set_curve(curve, config=self.gui.config)
449 def _postprocess_next_curve(self, command, args={}, results=[]):
450 """No-op. Only call 'next curve' via `self._next_curve()`.
454 def _postprocess_previous_curve(self, command, args={}, results=[]):
455 """No-op. Only call 'previous curve' via `self._previous_curve()`.
459 def _postprocess_zero_block_surface_contact_point(
460 self, command, args={}, results=[]):
461 """Update the curve, since the available columns may have changed.
463 if isinstance(results[-1], Success):
464 self.execute_command(
465 command=self._command_by_name('get curve'))
467 def _postprocess_add_block_force_array(
468 self, command, args={}, results=[]):
469 """Update the curve, since the available columns may have changed.
471 if isinstance(results[-1], Success):
472 self.execute_command(
473 command=self._command_by_name('get curve'))
479 def _GetActiveFileIndex(self):
480 lib.playlist.Playlist = self.GetActivePlaylist()
481 #get the selected item from the tree
482 selected_item = self._c['playlist']._c['tree'].GetSelection()
483 #test if a playlist or a curve was double-clicked
484 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
488 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
489 while selected_item.IsOk():
491 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
494 def _GetPlaylistTab(self, name):
495 for index, page in enumerate(self._c['notebook']._tabs._pages):
496 if page.caption == name:
500 def select_plugin(self, _class=None, method=None, plugin=None):
503 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
505 playlist = lib.playlist.Playlist(self, self.drivers)
507 playlist.add_curve(item)
508 if playlist.count > 0:
509 playlist.name = self._GetUniquePlaylistName(name)
511 self.AddTayliss(playlist)
513 def AppliesPlotmanipulator(self, name):
515 Returns True if the plotmanipulator 'name' is applied, False otherwise
516 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
518 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
520 def ApplyPlotmanipulators(self, plot, plot_file):
522 Apply all active plotmanipulators.
524 if plot is not None and plot_file is not None:
525 manipulated_plot = copy.deepcopy(plot)
526 for plotmanipulator in self.plotmanipulators:
527 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
528 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
529 return manipulated_plot
531 def GetActiveFigure(self):
532 playlist_name = self.GetActivePlaylistName()
533 figure = self.playlists[playlist_name].figure
534 if figure is not None:
538 def GetActiveFile(self):
539 playlist = self.GetActivePlaylist()
540 if playlist is not None:
541 return playlist.get_active_file()
544 def GetActivePlot(self):
545 playlist = self.GetActivePlaylist()
546 if playlist is not None:
547 return playlist.get_active_file().plot
550 def GetDisplayedPlot(self):
551 plot = copy.deepcopy(self.displayed_plot)
553 #plot.curves = copy.deepcopy(plot.curves)
556 def GetDisplayedPlotCorrected(self):
557 plot = copy.deepcopy(self.displayed_plot)
559 plot.curves = copy.deepcopy(plot.corrected_curves)
562 def GetDisplayedPlotRaw(self):
563 plot = copy.deepcopy(self.displayed_plot)
565 plot.curves = copy.deepcopy(plot.raw_curves)
568 def GetDockArt(self):
569 return self._c['manager'].GetArtProvider()
571 def GetPlotmanipulator(self, name):
573 Returns a plot manipulator function from its name
575 for plotmanipulator in self.plotmanipulators:
576 if plotmanipulator.name == name:
577 return plotmanipulator
580 def HasPlotmanipulator(self, name):
582 returns True if the plotmanipulator 'name' is loaded, False otherwise
584 for plotmanipulator in self.plotmanipulators:
585 if plotmanipulator.command == name:
590 def _on_dir_ctrl_left_double_click(self, event):
591 file_path = self.panelFolders.GetPath()
592 if os.path.isfile(file_path):
593 if file_path.endswith('.hkp'):
594 self.do_loadlist(file_path)
597 def _on_erase_background(self, event):
600 def _on_notebook_page_close(self, event):
601 ctrl = event.GetEventObject()
602 playlist_name = ctrl.GetPageText(ctrl._curpage)
603 self.DeleteFromPlaylists(playlist_name)
605 def OnPaneClose(self, event):
608 def OnPropGridChanged (self, event):
609 prop = event.GetProperty()
611 item_section = self.panelProperties.SelectedTreeItem
612 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
613 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
614 config = self.gui.config[plugin]
615 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
616 property_key = prop.GetName()
617 property_value = prop.GetDisplayedString()
619 config[property_section][property_key]['value'] = property_value
621 def OnResultsCheck(self, index, flag):
622 results = self.GetActivePlot().results
623 if results.has_key(self.results_str):
624 results[self.results_str].results[index].visible = flag
625 results[self.results_str].update()
629 def _on_size(self, event):
632 def UpdatePlaylistsTreeSelection(self):
633 playlist = self.GetActivePlaylist()
634 if playlist is not None:
635 if playlist.index >= 0:
636 self._c['status bar'].set_playlist(playlist)
640 def _on_curve_select(self, playlist, curve):
641 #create the plot tab and add playlist to the dictionary
642 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
643 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
644 #tab_index = self._c['notebook'].GetSelection()
645 playlist.figure = plotPanel.get_figure()
646 self.playlists[playlist.name] = playlist
647 #self.playlists[playlist.name] = [playlist, figure]
648 self._c['status bar'].set_playlist(playlist)
653 def _on_playlist_left_doubleclick(self):
654 index = self._c['notebook'].GetSelection()
655 current_playlist = self._c['notebook'].GetPageText(index)
656 if current_playlist != playlist_name:
657 index = self._GetPlaylistTab(playlist_name)
658 self._c['notebook'].SetSelection(index)
659 self._c['status bar'].set_playlist(playlist)
663 def _on_playlist_delete(self, playlist):
664 notebook = self.Parent.plotNotebook
665 index = self.Parent._GetPlaylistTab(playlist.name)
666 notebook.SetSelection(index)
667 notebook.DeletePage(notebook.GetSelection())
668 self.Parent.DeleteFromPlaylists(playlist_name)
672 # Command panel interface
674 def select_command(self, _class, method, command):
675 #self.select_plugin(plugin=command.plugin)
676 if 'assistant' in self._c:
677 self._c['assitant'].ChangeValue(command.help)
678 self._c['property editor'].clear()
679 self._c['property editor']._argument_from_label = {}
680 for argument in command.arguments:
681 if argument.name == 'help':
684 results = self.execute_command(
685 command=self._command_by_name('playlists'))
686 if not isinstance(results[-1], Success):
687 self._postprocess_text(command, results=results)
690 playlists = results[0]
692 results = self.execute_command(
693 command=self._command_by_name('playlist curves'))
694 if not isinstance(results[-1], Success):
695 self._postprocess_text(command, results=results)
700 ret = props_from_argument(
701 argument, curves=curves, playlists=playlists)
703 continue # property intentionally not handled (yet)
705 self._c['property editor'].append_property(p)
706 self._c['property editor']._argument_from_label[label] = (
709 self.gui.config['selected command'] = command # TODO: push to engine
713 # Note panel interface
715 def _on_update_note(self, _class, method, text):
716 """Sets the note for the active curve.
718 self.execute_command(
719 command=self._command_by_name('set note'),
724 # Playlist panel interface
726 def _on_user_delete_playlist(self, _class, method, playlist):
729 def _on_delete_playlist(self, _class, method, playlist):
730 if hasattr(playlist, 'path') and playlist.path != None:
731 os.remove(playlist.path)
733 def _on_user_delete_curve(self, _class, method, playlist, curve):
736 def _on_delete_curve(self, _class, method, playlist, curve):
737 os.remove(curve.path)
739 def _on_set_selected_playlist(self, _class, method, playlist):
740 """Call the `jump to playlist` command.
742 results = self.execute_command(
743 command=self._command_by_name('playlists'))
744 if not isinstance(results[-1], Success):
746 assert len(results) == 2, results
747 playlists = results[0]
748 matching = [p for p in playlists if p.name == playlist.name]
749 assert len(matching) == 1, matching
750 index = playlists.index(matching[0])
751 results = self.execute_command(
752 command=self._command_by_name('jump to playlist'),
753 args={'index':index})
755 def _on_set_selected_curve(self, _class, method, playlist, curve):
756 """Call the `jump to curve` command.
758 self._on_set_selected_playlist(_class, method, playlist)
759 index = playlist.index(curve)
760 results = self.execute_command(
761 command=self._command_by_name('jump to curve'),
762 args={'index':index})
763 if not isinstance(results[-1], Success):
765 #results = self.execute_command(
766 # command=self._command_by_name('get playlist'))
767 #if not isinstance(results[-1], Success):
769 self.execute_command(
770 command=self._command_by_name('get curve'))
776 def _next_curve(self, *args):
777 """Call the `next curve` command.
779 results = self.execute_command(
780 command=self._command_by_name('next curve'))
781 if isinstance(results[-1], Success):
782 self.execute_command(
783 command=self._command_by_name('get curve'))
785 def _previous_curve(self, *args):
786 """Call the `previous curve` command.
788 results = self.execute_command(
789 command=self._command_by_name('previous curve'))
790 if isinstance(results[-1], Success):
791 self.execute_command(
792 command=self._command_by_name('get curve'))
796 # Panel display handling
798 def _on_panel_visibility(self, _class, method, panel_name, visible):
799 pane = self._c['manager'].GetPane(panel_name)
801 #if we don't do the following, the Folders pane does not resize properly on hide/show
802 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
803 #folders_size = pane.GetSize()
804 self.panelFolders.Fit()
805 self._c['manager'].Update()
807 def _setup_perspectives(self):
808 """Add perspectives to menubar and _perspectives.
810 self._perspectives = {
811 'Default': self._c['manager'].SavePerspective(),
813 path = self.gui.config['perspective path']
814 if os.path.isdir(path):
815 files = sorted(os.listdir(path))
817 name, extension = os.path.splitext(fname)
818 if extension != self.gui.config['perspective extension']:
820 fpath = os.path.join(path, fname)
821 if not os.path.isfile(fpath):
824 with open(fpath, 'rU') as f:
825 perspective = f.readline()
827 self._perspectives[name] = perspective
829 selected_perspective = self.gui.config['active perspective']
830 if not self._perspectives.has_key(selected_perspective):
831 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
833 self._restore_perspective(selected_perspective, force=True)
834 self._update_perspective_menu()
836 def _update_perspective_menu(self):
837 self._c['menu bar']._c['perspective'].update(
838 sorted(self._perspectives.keys()),
839 self.gui.config['active perspective'])
841 def _save_perspective(self, perspective, perspective_dir, name,
843 path = os.path.join(perspective_dir, name)
844 if extension != None:
846 if not os.path.isdir(perspective_dir):
847 os.makedirs(perspective_dir)
848 with open(path, 'w') as f:
850 self._perspectives[name] = perspective
851 self._restore_perspective(name)
852 self._update_perspective_menu()
854 def _delete_perspectives(self, perspective_dir, names,
856 self.log.debug('remove perspectives %s from %s'
857 % (names, perspective_dir))
859 path = os.path.join(perspective_dir, name)
860 if extension != None:
863 del(self._perspectives[name])
864 self._update_perspective_menu()
865 if self.gui.config['active perspective'] in names:
866 self._restore_perspective('Default')
867 # TODO: does this bug still apply?
868 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
869 # http://trac.wxwidgets.org/ticket/3258
870 # ) that makes the radio item indicator in the menu disappear.
871 # The code should be fine once this issue is fixed.
873 def _restore_perspective(self, name, force=False):
874 if name != self.gui.config['active perspective'] or force == True:
875 self.log.debug('restore perspective %s' % name)
876 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
877 self._c['manager'].LoadPerspective(self._perspectives[name])
878 self._c['manager'].Update()
879 for pane in self._c['manager'].GetAllPanes():
880 view = self._c['menu bar']._c['view']
881 if pane.name in view._c.keys():
882 view._c[pane.name].Check(pane.window.IsShown())
884 def _on_save_perspective(self, *args):
885 perspective = self._c['manager'].SavePerspective()
886 name = self.gui.config['active perspective']
887 if name == 'Default':
888 name = 'New perspective'
889 name = select_save_file(
890 directory=self.gui.config['perspective path'],
892 extension=self.gui.config['perspective extension'],
894 message='Enter a name for the new perspective:',
895 caption='Save perspective')
898 self._save_perspective(
899 perspective, self.gui.config['perspective path'], name=name,
900 extension=self.gui.config['perspective extension'])
902 def _on_delete_perspective(self, *args, **kwargs):
903 options = sorted([p for p in self._perspectives.keys()
905 dialog = SelectionDialog(
907 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
908 button_id=wx.ID_DELETE,
909 selection_style='multiple',
911 title='Delete perspective(s)',
912 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
913 dialog.CenterOnScreen()
915 names = [options[i] for i in dialog.selected]
917 self._delete_perspectives(
918 self.gui.config['perspective path'], names=names,
919 extension=self.gui.config['perspective extension'])
921 def _on_select_perspective(self, _class, method, name):
922 self._restore_perspective(name)
926 class HookeApp (wx.App):
927 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
929 Tosses up a splash screen and then loads :class:`HookeFrame` in
932 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
934 self.commands = commands
935 self.inqueue = inqueue
936 self.outqueue = outqueue
937 super(HookeApp, self).__init__(*args, **kwargs)
940 self.SetAppName('Hooke')
941 self.SetVendorName('')
942 self._setup_splash_screen()
944 height = self.gui.config['main height']
945 width = self.gui.config['main width']
946 top = self.gui.config['main top']
947 left = self.gui.config['main left']
949 # Sometimes, the ini file gets confused and sets 'left' and
950 # 'top' to large negative numbers. Here we catch and fix
951 # this. Keep small negative numbers, the user might want
960 self.gui, self.commands, self.inqueue, self.outqueue,
961 parent=None, title='Hooke',
962 pos=(left, top), size=(width, height),
963 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
965 self._c['frame'].Show(True)
966 self.SetTopWindow(self._c['frame'])
969 def _setup_splash_screen(self):
970 if self.gui.config['show splash screen'] == True:
971 path = self.gui.config['splash screen image']
972 if os.path.isfile(path):
973 duration = self.gui.config['splash screen duration']
975 bitmap=wx.Image(path).ConvertToBitmap(),
976 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
977 milliseconds=duration,
980 # For some reason splashDuration and sleep do not
981 # correspond to each other at least not on Windows.
982 # Maybe it's because duration is in milliseconds and
983 # sleep in seconds. Thus we need to increase the
984 # sleep time a bit. A factor of 1.2 seems to work.
986 time.sleep(sleepFactor * duration / 1000)
989 class GUI (UserInterface):
990 """wxWindows graphical user interface.
993 super(GUI, self).__init__(name='gui')
995 def default_settings(self):
996 """Return a list of :class:`hooke.config.Setting`\s for any
997 configurable UI settings.
999 The suggested section setting is::
1001 Setting(section=self.setting_section, help=self.__doc__)
1004 Setting(section=self.setting_section, help=self.__doc__),
1005 Setting(section=self.setting_section, option='icon image',
1006 value=os.path.join('doc', 'img', 'microscope.ico'),
1008 help='Path to the hooke icon image.'),
1009 Setting(section=self.setting_section, option='show splash screen',
1010 value=True, type='bool',
1011 help='Enable/disable the splash screen'),
1012 Setting(section=self.setting_section, option='splash screen image',
1013 value=os.path.join('doc', 'img', 'hooke.jpg'),
1015 help='Path to the Hooke splash screen image.'),
1016 Setting(section=self.setting_section,
1017 option='splash screen duration',
1018 value=1000, type='int',
1019 help='Duration of the splash screen in milliseconds.'),
1020 Setting(section=self.setting_section, option='perspective path',
1021 value=os.path.join('resources', 'gui', 'perspective'),
1022 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1023 Setting(section=self.setting_section, option='perspective extension',
1025 help='Extension for perspective files.'),
1026 Setting(section=self.setting_section, option='hide extensions',
1027 value=False, type='bool',
1028 help='Hide file extensions when displaying names.'),
1029 Setting(section=self.setting_section, option='plot legend',
1030 value=True, type='bool',
1031 help='Enable/disable the plot legend.'),
1032 Setting(section=self.setting_section, option='plot SI format',
1033 value='True', type='bool',
1034 help='Enable/disable SI plot axes numbering.'),
1035 Setting(section=self.setting_section, option='plot decimals',
1036 value=2, type='int',
1037 help='Number of decimal places to show if "plot SI format" is enabled.'),
1038 Setting(section=self.setting_section, option='folders-workdir',
1039 value='.', type='path',
1040 help='This should probably go...'),
1041 Setting(section=self.setting_section, option='folders-filters',
1042 value='.', type='path',
1043 help='This should probably go...'),
1044 Setting(section=self.setting_section, option='active perspective',
1046 help='Name of active perspective file (or "Default").'),
1047 Setting(section=self.setting_section,
1048 option='folders-filter-index',
1049 value=0, type='int',
1050 help='This should probably go...'),
1051 Setting(section=self.setting_section, option='main height',
1052 value=450, type='int',
1053 help='Height of main window in pixels.'),
1054 Setting(section=self.setting_section, option='main width',
1055 value=800, type='int',
1056 help='Width of main window in pixels.'),
1057 Setting(section=self.setting_section, option='main top',
1058 value=0, type='int',
1059 help='Pixels from screen top to top of main window.'),
1060 Setting(section=self.setting_section, option='main left',
1061 value=0, type='int',
1062 help='Pixels from screen left to left of main window.'),
1063 Setting(section=self.setting_section, option='selected command',
1064 value='load playlist',
1065 help='Name of the initially selected command.'),
1068 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1072 app = HookeApp(gui=self,
1074 inqueue=ui_to_command_queue,
1075 outqueue=command_to_ui_queue,
1079 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1080 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)