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()
117 return # TODO: cleanup
118 self.playlists = self._c['playlist'].Playlists
119 self._displayed_plot = None
120 #load default list, if possible
121 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
126 def _setup_panels(self):
127 client_size = self.GetClientSize()
129 # ('folders', wx.GenericDirCtrl(
131 # dir=self.gui.config['folders-workdir'],
133 # style=wx.DIRCTRL_SHOW_FILTERS,
134 # filter=self.gui.config['folders-filters'],
135 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
136 (panel.PANELS['playlist'](
138 'delete_playlist':self._on_user_delete_playlist,
139 '_delete_playlist':self._on_delete_playlist,
140 'delete_curve':self._on_user_delete_curve,
141 '_delete_curve':self._on_delete_curve,
142 '_on_set_selected_playlist':self._on_set_selected_playlist,
143 '_on_set_selected_curve':self._on_set_selected_curve,
146 style=wx.WANTS_CHARS|wx.NO_BORDER,
147 # WANTS_CHARS so the panel doesn't eat the Return key.
150 (panel.PANELS['note'](
152 '_on_update':self._on_update_note,
155 style=wx.WANTS_CHARS|wx.NO_BORDER,
158 # ('notebook', Notebook(
160 # pos=wx.Point(client_size.x, client_size.y),
161 # size=wx.Size(430, 200),
162 # style=aui.AUI_NB_DEFAULT_STYLE
163 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
164 (panel.PANELS['commands'](
165 commands=self.commands,
166 selected=self.gui.config['selected command'],
168 'execute': self.execute_command,
169 'select_plugin': self.select_plugin,
170 'select_command': self.select_command,
171 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
174 style=wx.WANTS_CHARS|wx.NO_BORDER,
175 # WANTS_CHARS so the panel doesn't eat the Return key.
178 (panel.PANELS['propertyeditor'](
181 style=wx.WANTS_CHARS,
182 # WANTS_CHARS so the panel doesn't eat the Return key.
184 (panel.PANELS['plot'](
186 '_set_status_text': self._on_plot_status_text,
189 style=wx.WANTS_CHARS|wx.NO_BORDER,
190 # WANTS_CHARS so the panel doesn't eat the Return key.
193 (panel.PANELS['output'](
196 size=wx.Size(150, 90),
197 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
199 # ('results', panel.results.Results(self), 'bottom'),
201 self._add_panel(p, style)
203 def _add_panel(self, panel, style):
204 self._c[panel.name] = panel
205 m_name = panel.managed_name
206 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
207 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
210 elif style == 'center':
212 elif style == 'left':
214 elif style == 'right':
217 assert style == 'bottom', style
219 self._c['manager'].AddPane(panel, info)
221 def _setup_toolbars(self):
222 self._c['navigation bar'] = navbar.NavBar(
224 'next': self._next_curve,
225 'previous': self._previous_curve,
228 style=wx.TB_FLAT | wx.TB_NODIVIDER)
229 self._c['manager'].AddPane(
230 self._c['navigation bar'],
231 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
232 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
233 ).RightDockable(False))
235 def _bind_events(self):
236 # TODO: figure out if we can use the eventManager for menu
237 # ranges and events of 'self' without raising an assertion
239 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
240 self.Bind(wx.EVT_SIZE, self._on_size)
241 self.Bind(wx.EVT_CLOSE, self._on_close)
242 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
243 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
245 return # TODO: cleanup
246 treeCtrl = self._c['folders'].GetTreeCtrl()
247 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
250 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
252 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
254 def _on_about(self, *args):
255 dialog = wx.MessageDialog(
257 message=self.gui._splash_text(extra_info={
258 'get-details':'click "Help -> License"'},
260 caption='About Hooke',
261 style=wx.OK|wx.ICON_INFORMATION)
265 def _on_close(self, *args):
266 self.log.info('closing GUI framework')
268 self.gui.config['main height'] = str(self.GetSize().GetHeight())
269 self.gui.config['main left'] = str(self.GetPosition()[0])
270 self.gui.config['main top'] = str(self.GetPosition()[1])
271 self.gui.config['main width'] = str(self.GetSize().GetWidth())
272 # push changes back to Hooke.config?
273 self._c['manager'].UnInit()
274 del self._c['manager']
279 # Panel utility functions
281 def _file_name(self, name):
282 """Cleanup names according to configured preferences.
284 if self.gui.config['hide extensions'] == True:
285 name,ext = os.path.splitext(name)
292 def _command_by_name(self, name):
293 cs = [c for c in self.commands if c.name == name]
297 raise Exception('Multiple commands named "%s"' % name)
300 def execute_command(self, _class=None, method=None,
301 command=None, args=None):
304 if ('property editor' in self._c
305 and self.gui.config['selected command'] == command):
306 for name,value in self._c['property editor'].get_values().items():
307 arg = self._c['property editor']._argument_from_label.get(
312 args[arg.name] = value
314 # deal with counted arguments
315 if arg.name not in args:
317 index = int(name[len(arg.name):])
318 args[arg.name][index] = value
319 for arg in command.arguments:
321 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
322 count = arg._display_count
323 if count != 1 and arg.name in args:
324 keys = sorted(args[arg.name].keys())
325 assert keys == range(count), keys
326 args[arg.name] = [args[arg.name][i]
327 for i in range(count)]
329 while (len(args[arg.name]) > 0
330 and args[arg.name][-1] == None):
332 if len(args[arg.name]) == 0:
333 args[arg.name] = arg.default
334 self.log.debug('executing %s with %s' % (command.name, args))
335 self.inqueue.put(CommandMessage(command, args))
338 msg = self.outqueue.get()
340 if isinstance(msg, Exit):
343 elif isinstance(msg, CommandExit):
344 # TODO: display command complete
346 elif isinstance(msg, ReloadUserInterfaceConfig):
347 self.gui.reload_config(msg.config)
349 elif isinstance(msg, Request):
350 h = handler.HANDLERS[msg.type]
351 h.run(self, msg) # TODO: pause for response?
354 self, '_postprocess_%s' % command.name.replace(' ', '_'),
355 self._postprocess_text)
356 pp(command=command, args=args, results=results)
359 def _handle_request(self, msg):
360 """Repeatedly try to get a response to `msg`.
363 raise NotImplementedError('_%s_request_prompt' % msg.type)
364 prompt_string = prompt(msg)
365 parser = getattr(self, '_%s_request_parser' % msg.type, None)
367 raise NotImplementedError('_%s_request_parser' % msg.type)
371 self.cmd.stdout.write(''.join([
372 error.__class__.__name__, ': ', str(error), '\n']))
373 self.cmd.stdout.write(prompt_string)
374 value = parser(msg, self.cmd.stdin.readline())
376 response = msg.response(value)
378 except ValueError, error:
380 self.inqueue.put(response)
384 # Command-specific postprocessing
386 def _postprocess_text(self, command, args={}, results=[]):
387 """Print the string representation of the results to the Results window.
389 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
390 approach, except that :class:`~hooke.ui.commandline.DoCommand`
391 doesn't print some internally handled messages
392 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
394 for result in results:
395 if isinstance(result, CommandExit):
396 self._c['output'].write(result.__class__.__name__+'\n')
397 self._c['output'].write(str(result).rstrip()+'\n')
399 def _postprocess_load_playlist(self, command, args={}, results=None):
400 """Update `self` to show the playlist.
402 if not isinstance(results[-1], Success):
403 self._postprocess_text(command, results=results)
405 assert len(results) == 2, results
406 playlist = results[0]
407 self._c['playlist']._c['tree'].add_playlist(playlist)
409 def _postprocess_get_playlist(self, command, args={}, results=[]):
410 if not isinstance(results[-1], Success):
411 self._postprocess_text(command, results=results)
413 assert len(results) == 2, results
414 playlist = results[0]
415 self._c['playlist']._c['tree'].update_playlist(playlist)
417 def _postprocess_get_curve(self, command, args={}, results=[]):
418 """Update `self` to show the curve.
420 if not isinstance(results[-1], Success):
421 self._postprocess_text(command, results=results)
423 assert len(results) == 2, results
425 if args.get('curve', None) == None:
426 # the command defaults to the current curve of the current playlist
427 results = self.execute_command(
428 command=self._command_by_name('get playlist'))
429 playlist = results[0]
431 raise NotImplementedError()
432 if 'note' in self._c:
433 self._c['note'].set_text(curve.info['note'])
434 if 'playlist' in self._c:
435 self._c['playlist']._c['tree'].set_selected_curve(
437 if 'plot' in self._c:
438 self._c['plot'].set_curve(curve, config=self.gui.config)
440 def _postprocess_next_curve(self, command, args={}, results=[]):
441 """No-op. Only call 'next curve' via `self._next_curve()`.
445 def _postprocess_previous_curve(self, command, args={}, results=[]):
446 """No-op. Only call 'previous curve' via `self._previous_curve()`.
450 def _postprocess_zero_block_surface_contact_point(
451 self, command, args={}, results=[]):
452 """Update the curve, since the available columns may have changed.
454 if isinstance(results[-1], Success):
455 self.execute_command(
456 command=self._command_by_name('get curve'))
458 def _postprocess_add_block_force_array(
459 self, command, args={}, results=[]):
460 """Update the curve, since the available columns may have changed.
462 if isinstance(results[-1], Success):
463 self.execute_command(
464 command=self._command_by_name('get curve'))
470 def _GetActiveFileIndex(self):
471 lib.playlist.Playlist = self.GetActivePlaylist()
472 #get the selected item from the tree
473 selected_item = self._c['playlist']._c['tree'].GetSelection()
474 #test if a playlist or a curve was double-clicked
475 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
479 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
480 while selected_item.IsOk():
482 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
485 def _GetPlaylistTab(self, name):
486 for index, page in enumerate(self._c['notebook']._tabs._pages):
487 if page.caption == name:
491 def select_plugin(self, _class=None, method=None, plugin=None):
494 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
496 playlist = lib.playlist.Playlist(self, self.drivers)
498 playlist.add_curve(item)
499 if playlist.count > 0:
500 playlist.name = self._GetUniquePlaylistName(name)
502 self.AddTayliss(playlist)
504 def AppliesPlotmanipulator(self, name):
506 Returns True if the plotmanipulator 'name' is applied, False otherwise
507 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
509 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
511 def ApplyPlotmanipulators(self, plot, plot_file):
513 Apply all active plotmanipulators.
515 if plot is not None and plot_file is not None:
516 manipulated_plot = copy.deepcopy(plot)
517 for plotmanipulator in self.plotmanipulators:
518 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
519 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
520 return manipulated_plot
522 def GetActiveFigure(self):
523 playlist_name = self.GetActivePlaylistName()
524 figure = self.playlists[playlist_name].figure
525 if figure is not None:
529 def GetActiveFile(self):
530 playlist = self.GetActivePlaylist()
531 if playlist is not None:
532 return playlist.get_active_file()
535 def GetActivePlot(self):
536 playlist = self.GetActivePlaylist()
537 if playlist is not None:
538 return playlist.get_active_file().plot
541 def GetDisplayedPlot(self):
542 plot = copy.deepcopy(self.displayed_plot)
544 #plot.curves = copy.deepcopy(plot.curves)
547 def GetDisplayedPlotCorrected(self):
548 plot = copy.deepcopy(self.displayed_plot)
550 plot.curves = copy.deepcopy(plot.corrected_curves)
553 def GetDisplayedPlotRaw(self):
554 plot = copy.deepcopy(self.displayed_plot)
556 plot.curves = copy.deepcopy(plot.raw_curves)
559 def GetDockArt(self):
560 return self._c['manager'].GetArtProvider()
562 def GetPlotmanipulator(self, name):
564 Returns a plot manipulator function from its name
566 for plotmanipulator in self.plotmanipulators:
567 if plotmanipulator.name == name:
568 return plotmanipulator
571 def HasPlotmanipulator(self, name):
573 returns True if the plotmanipulator 'name' is loaded, False otherwise
575 for plotmanipulator in self.plotmanipulators:
576 if plotmanipulator.command == name:
581 def _on_dir_ctrl_left_double_click(self, event):
582 file_path = self.panelFolders.GetPath()
583 if os.path.isfile(file_path):
584 if file_path.endswith('.hkp'):
585 self.do_loadlist(file_path)
588 def _on_erase_background(self, event):
591 def _on_notebook_page_close(self, event):
592 ctrl = event.GetEventObject()
593 playlist_name = ctrl.GetPageText(ctrl._curpage)
594 self.DeleteFromPlaylists(playlist_name)
596 def OnPaneClose(self, event):
599 def OnPropGridChanged (self, event):
600 prop = event.GetProperty()
602 item_section = self.panelProperties.SelectedTreeItem
603 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
604 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
605 config = self.gui.config[plugin]
606 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
607 property_key = prop.GetName()
608 property_value = prop.GetDisplayedString()
610 config[property_section][property_key]['value'] = property_value
612 def OnResultsCheck(self, index, flag):
613 results = self.GetActivePlot().results
614 if results.has_key(self.results_str):
615 results[self.results_str].results[index].visible = flag
616 results[self.results_str].update()
620 def _on_size(self, event):
623 def UpdatePlaylistsTreeSelection(self):
624 playlist = self.GetActivePlaylist()
625 if playlist is not None:
626 if playlist.index >= 0:
627 self._c['status bar'].set_playlist(playlist)
631 def _on_curve_select(self, playlist, curve):
632 #create the plot tab and add playlist to the dictionary
633 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
634 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
635 #tab_index = self._c['notebook'].GetSelection()
636 playlist.figure = plotPanel.get_figure()
637 self.playlists[playlist.name] = playlist
638 #self.playlists[playlist.name] = [playlist, figure]
639 self._c['status bar'].set_playlist(playlist)
644 def _on_playlist_left_doubleclick(self):
645 index = self._c['notebook'].GetSelection()
646 current_playlist = self._c['notebook'].GetPageText(index)
647 if current_playlist != playlist_name:
648 index = self._GetPlaylistTab(playlist_name)
649 self._c['notebook'].SetSelection(index)
650 self._c['status bar'].set_playlist(playlist)
654 def _on_playlist_delete(self, playlist):
655 notebook = self.Parent.plotNotebook
656 index = self.Parent._GetPlaylistTab(playlist.name)
657 notebook.SetSelection(index)
658 notebook.DeletePage(notebook.GetSelection())
659 self.Parent.DeleteFromPlaylists(playlist_name)
663 # Command panel interface
665 def select_command(self, _class, method, command):
666 #self.select_plugin(plugin=command.plugin)
667 self._c['property editor'].clear()
668 self._c['property editor']._argument_from_label = {}
669 for argument in command.arguments:
670 if argument.name == 'help':
673 results = self.execute_command(
674 command=self._command_by_name('playlists'))
675 if not isinstance(results[-1], Success):
676 self._postprocess_text(command, results=results)
679 playlists = results[0]
681 results = self.execute_command(
682 command=self._command_by_name('playlist curves'))
683 if not isinstance(results[-1], Success):
684 self._postprocess_text(command, results=results)
689 ret = props_from_argument(
690 argument, curves=curves, playlists=playlists)
692 continue # property intentionally not handled (yet)
694 self._c['property editor'].append_property(p)
695 self._c['property editor']._argument_from_label[label] = (
698 self.gui.config['selected command'] = command # TODO: push to engine
702 # Note panel interface
704 def _on_update_note(self, _class, method, text):
705 """Sets the note for the active curve.
707 self.execute_command(
708 command=self._command_by_name('set note'),
713 # Playlist panel interface
715 def _on_user_delete_playlist(self, _class, method, playlist):
718 def _on_delete_playlist(self, _class, method, playlist):
719 if hasattr(playlist, 'path') and playlist.path != None:
720 os.remove(playlist.path)
722 def _on_user_delete_curve(self, _class, method, playlist, curve):
725 def _on_delete_curve(self, _class, method, playlist, curve):
726 # TODO: execute_command 'remove curve from playlist'
727 os.remove(curve.path)
729 def _on_set_selected_playlist(self, _class, method, playlist):
730 """Call the `jump to playlist` command.
732 results = self.execute_command(
733 command=self._command_by_name('playlists'))
734 if not isinstance(results[-1], Success):
736 assert len(results) == 2, results
737 playlists = results[0]
738 matching = [p for p in playlists if p.name == playlist.name]
739 assert len(matching) == 1, matching
740 index = playlists.index(matching[0])
741 results = self.execute_command(
742 command=self._command_by_name('jump to playlist'),
743 args={'index':index})
745 def _on_set_selected_curve(self, _class, method, playlist, curve):
746 """Call the `jump to curve` command.
748 self._on_set_selected_playlist(_class, method, playlist)
749 index = playlist.index(curve)
750 results = self.execute_command(
751 command=self._command_by_name('jump to curve'),
752 args={'index':index})
753 if not isinstance(results[-1], Success):
755 #results = self.execute_command(
756 # command=self._command_by_name('get playlist'))
757 #if not isinstance(results[-1], Success):
759 self.execute_command(
760 command=self._command_by_name('get curve'))
764 # Plot panel interface
766 def _on_plot_status_text(self, _class, method, text):
767 if 'status bar' in self._c:
768 self._c['status bar'].set_plot_text(text)
774 def _next_curve(self, *args):
775 """Call the `next curve` command.
777 results = self.execute_command(
778 command=self._command_by_name('next curve'))
779 if isinstance(results[-1], Success):
780 self.execute_command(
781 command=self._command_by_name('get curve'))
783 def _previous_curve(self, *args):
784 """Call the `previous curve` command.
786 results = self.execute_command(
787 command=self._command_by_name('previous curve'))
788 if isinstance(results[-1], Success):
789 self.execute_command(
790 command=self._command_by_name('get curve'))
794 # Panel display handling
796 def _on_panel_visibility(self, _class, method, panel_name, visible):
797 pane = self._c['manager'].GetPane(panel_name)
799 #if we don't do the following, the Folders pane does not resize properly on hide/show
800 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
801 #folders_size = pane.GetSize()
802 self.panelFolders.Fit()
803 self._c['manager'].Update()
805 def _setup_perspectives(self):
806 """Add perspectives to menubar and _perspectives.
808 self._perspectives = {
809 'Default': self._c['manager'].SavePerspective(),
811 path = self.gui.config['perspective path']
812 if os.path.isdir(path):
813 files = sorted(os.listdir(path))
815 name, extension = os.path.splitext(fname)
816 if extension != self.gui.config['perspective extension']:
818 fpath = os.path.join(path, fname)
819 if not os.path.isfile(fpath):
822 with open(fpath, 'rU') as f:
823 perspective = f.readline()
825 self._perspectives[name] = perspective
827 selected_perspective = self.gui.config['active perspective']
828 if not self._perspectives.has_key(selected_perspective):
829 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
831 self._restore_perspective(selected_perspective, force=True)
832 self._update_perspective_menu()
834 def _update_perspective_menu(self):
835 self._c['menu bar']._c['perspective'].update(
836 sorted(self._perspectives.keys()),
837 self.gui.config['active perspective'])
839 def _save_perspective(self, perspective, perspective_dir, name,
841 path = os.path.join(perspective_dir, name)
842 if extension != None:
844 if not os.path.isdir(perspective_dir):
845 os.makedirs(perspective_dir)
846 with open(path, 'w') as f:
848 self._perspectives[name] = perspective
849 self._restore_perspective(name)
850 self._update_perspective_menu()
852 def _delete_perspectives(self, perspective_dir, names,
854 self.log.debug('remove perspectives %s from %s'
855 % (names, perspective_dir))
857 path = os.path.join(perspective_dir, name)
858 if extension != None:
861 del(self._perspectives[name])
862 self._update_perspective_menu()
863 if self.gui.config['active perspective'] in names:
864 self._restore_perspective('Default')
865 # TODO: does this bug still apply?
866 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
867 # http://trac.wxwidgets.org/ticket/3258
868 # ) that makes the radio item indicator in the menu disappear.
869 # The code should be fine once this issue is fixed.
871 def _restore_perspective(self, name, force=False):
872 if name != self.gui.config['active perspective'] or force == True:
873 self.log.debug('restore perspective %s' % name)
874 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
875 self._c['manager'].LoadPerspective(self._perspectives[name])
876 self._c['manager'].Update()
877 for pane in self._c['manager'].GetAllPanes():
878 view = self._c['menu bar']._c['view']
879 if pane.name in view._c.keys():
880 view._c[pane.name].Check(pane.window.IsShown())
882 def _on_save_perspective(self, *args):
883 perspective = self._c['manager'].SavePerspective()
884 name = self.gui.config['active perspective']
885 if name == 'Default':
886 name = 'New perspective'
887 name = select_save_file(
888 directory=self.gui.config['perspective path'],
890 extension=self.gui.config['perspective extension'],
892 message='Enter a name for the new perspective:',
893 caption='Save perspective')
896 self._save_perspective(
897 perspective, self.gui.config['perspective path'], name=name,
898 extension=self.gui.config['perspective extension'])
900 def _on_delete_perspective(self, *args, **kwargs):
901 options = sorted([p for p in self._perspectives.keys()
903 dialog = SelectionDialog(
905 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
906 button_id=wx.ID_DELETE,
907 selection_style='multiple',
909 title='Delete perspective(s)',
910 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
911 dialog.CenterOnScreen()
913 if dialog.canceled == True:
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)