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 prop_from_argument, prop_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/vclamp_picoforce/playlist'},
122 return # TODO: cleanup
123 self.playlists = self._c['playlist'].Playlists
124 self._displayed_plot = None
125 #load default list, if possible
126 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
131 def _setup_panels(self):
132 client_size = self.GetClientSize()
134 # ('folders', wx.GenericDirCtrl(
136 # dir=self.gui.config['folders-workdir'],
138 # style=wx.DIRCTRL_SHOW_FILTERS,
139 # filter=self.gui.config['folders-filters'],
140 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
141 (panel.PANELS['playlist'](
143 'delete_playlist':self._on_user_delete_playlist,
144 '_delete_playlist':self._on_delete_playlist,
145 'delete_curve':self._on_user_delete_curve,
146 '_delete_curve':self._on_delete_curve,
147 '_on_set_selected_playlist':self._on_set_selected_playlist,
148 '_on_set_selected_curve':self._on_set_selected_curve,
151 style=wx.WANTS_CHARS|wx.NO_BORDER,
152 # WANTS_CHARS so the panel doesn't eat the Return key.
155 (panel.PANELS['note'](
157 '_on_update':self._on_update_note,
160 style=wx.WANTS_CHARS|wx.NO_BORDER,
163 # ('notebook', Notebook(
165 # pos=wx.Point(client_size.x, client_size.y),
166 # size=wx.Size(430, 200),
167 # style=aui.AUI_NB_DEFAULT_STYLE
168 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
169 (panel.PANELS['commands'](
170 commands=self.commands,
171 selected=self.gui.config['selected command'],
173 'execute': self.execute_command,
174 'select_plugin': self.select_plugin,
175 'select_command': self.select_command,
176 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
179 style=wx.WANTS_CHARS|wx.NO_BORDER,
180 # WANTS_CHARS so the panel doesn't eat the Return key.
183 (panel.PANELS['propertyeditor'](
186 style=wx.WANTS_CHARS,
187 # WANTS_CHARS so the panel doesn't eat the Return key.
189 # ('assistant', wx.TextCtrl(
191 # pos=wx.Point(0, 0),
192 # size=wx.Size(150, 90),
193 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
194 (panel.PANELS['plot'](
198 style=wx.WANTS_CHARS|wx.NO_BORDER,
199 # WANTS_CHARS so the panel doesn't eat the Return key.
202 (panel.PANELS['output'](
205 size=wx.Size(150, 90),
206 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
208 # ('results', panel.results.Results(self), 'bottom'),
210 self._add_panel(p, style)
211 #self._c['assistant'].SetEditable(False)
213 def _add_panel(self, panel, style):
214 self._c[panel.name] = panel
215 m_name = panel.managed_name
216 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
217 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
220 elif style == 'center':
222 elif style == 'left':
224 elif style == 'right':
227 assert style == 'bottom', style
229 self._c['manager'].AddPane(panel, info)
231 def _setup_toolbars(self):
232 self._c['navigation bar'] = navbar.NavBar(
234 'next': self._next_curve,
235 'previous': self._previous_curve,
238 style=wx.TB_FLAT | wx.TB_NODIVIDER)
239 self._c['manager'].AddPane(
240 self._c['navigation bar'],
241 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
242 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
243 ).RightDockable(False))
245 def _bind_events(self):
246 # TODO: figure out if we can use the eventManager for menu
247 # ranges and events of 'self' without raising an assertion
249 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
250 self.Bind(wx.EVT_SIZE, self._on_size)
251 self.Bind(wx.EVT_CLOSE, self._on_close)
252 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
253 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
255 return # TODO: cleanup
256 treeCtrl = self._c['folders'].GetTreeCtrl()
257 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
260 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
262 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
264 def _on_about(self, *args):
265 dialog = wx.MessageDialog(
267 message=self.gui._splash_text(extra_info={
268 'get-details':'click "Help -> License"'},
270 caption='About Hooke',
271 style=wx.OK|wx.ICON_INFORMATION)
275 def _on_close(self, *args):
276 self.log.info('closing GUI framework')
278 self.gui.config['main height'] = str(self.GetSize().GetHeight())
279 self.gui.config['main left'] = str(self.GetPosition()[0])
280 self.gui.config['main top'] = str(self.GetPosition()[1])
281 self.gui.config['main width'] = str(self.GetSize().GetWidth())
282 # push changes back to Hooke.config?
283 self._c['manager'].UnInit()
284 del self._c['manager']
289 # Panel utility functions
291 def _file_name(self, name):
292 """Cleanup names according to configured preferences.
294 if self.gui.config['hide extensions'] == 'True': # HACK: config should decode
295 name,ext = os.path.splitext(name)
302 def _command_by_name(self, name):
303 cs = [c for c in self.commands if c.name == name]
307 raise Exception('Multiple commands named "%s"' % name)
310 def execute_command(self, _class=None, method=None,
311 command=None, args=None):
314 if ('property editor' in self._c
315 and self.gui.config['selected command'] == command):
316 arg_names = [arg.name for arg in command.arguments]
317 for name,value in self._c['property editor'].get_values().items():
318 if name in arg_names:
320 self.log.debug('executing %s with %s' % (command.name, args))
321 self.inqueue.put(CommandMessage(command, args))
324 msg = self.outqueue.get()
326 if isinstance(msg, Exit):
329 elif isinstance(msg, CommandExit):
330 # TODO: display command complete
332 elif isinstance(msg, ReloadUserInterfaceConfig):
333 self.gui.reload_config(msg.config)
335 elif isinstance(msg, Request):
336 h = handler.HANDLERS[msg.type]
337 h.run(self, msg) # TODO: pause for response?
340 self, '_postprocess_%s' % command.name.replace(' ', '_'),
341 self._postprocess_text)
342 pp(command=command, args=args, results=results)
345 def _handle_request(self, msg):
346 """Repeatedly try to get a response to `msg`.
349 raise NotImplementedError('_%s_request_prompt' % msg.type)
350 prompt_string = prompt(msg)
351 parser = getattr(self, '_%s_request_parser' % msg.type, None)
353 raise NotImplementedError('_%s_request_parser' % msg.type)
357 self.cmd.stdout.write(''.join([
358 error.__class__.__name__, ': ', str(error), '\n']))
359 self.cmd.stdout.write(prompt_string)
360 value = parser(msg, self.cmd.stdin.readline())
362 response = msg.response(value)
364 except ValueError, error:
366 self.inqueue.put(response)
370 # Command-specific postprocessing
372 def _postprocess_text(self, command, args={}, results=[]):
373 """Print the string representation of the results to the Results window.
375 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
376 approach, except that :class:`~hooke.ui.commandline.DoCommand`
377 doesn't print some internally handled messages
378 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
380 for result in results:
381 if isinstance(result, CommandExit):
382 self._c['output'].write(result.__class__.__name__+'\n')
383 self._c['output'].write(str(result).rstrip()+'\n')
385 def _postprocess_load_playlist(self, command, args={}, results=None):
386 """Update `self` to show the playlist.
388 if not isinstance(results[-1], Success):
389 self._postprocess_text(command, results=results)
391 assert len(results) == 2, results
392 playlist = results[0]
393 self._c['playlist']._c['tree'].add_playlist(playlist)
395 def _postprocess_get_playlist(self, command, args={}, results=[]):
396 if not isinstance(results[-1], Success):
397 self._postprocess_text(command, results=results)
399 assert len(results) == 2, results
400 playlist = results[0]
401 self._c['playlist']._c['tree'].update_playlist(playlist)
403 def _postprocess_get_curve(self, command, args={}, results=[]):
404 """Update `self` to show the curve.
406 if not isinstance(results[-1], Success):
407 self._postprocess_text(command, results=results)
409 assert len(results) == 2, results
411 if args.get('curve', None) == None:
412 # the command defaults to the current curve of the current playlist
413 results = self.execute_command(
414 command=self._command_by_name('get playlist'))
415 playlist = results[0]
417 raise NotImplementedError()
418 if 'note' in self._c:
419 self._c['note'].set_text(curve.info['note'])
420 if 'playlist' in self._c:
421 self._c['playlist']._c['tree'].set_selected_curve(
423 if 'plot' in self._c:
424 self._c['plot'].set_curve(curve, config=self.gui.config)
426 def _postprocess_next_curve(self, command, args={}, results=[]):
427 """No-op. Only call 'next curve' via `self._next_curve()`.
431 def _postprocess_previous_curve(self, command, args={}, results=[]):
432 """No-op. Only call 'previous curve' via `self._previous_curve()`.
436 def _postprocess_zero_block_surface_contact_point(
437 self, command, args={}, results=[]):
438 """Update the curve, since the available columns may have changed.
440 if isinstance(results[-1], Success):
441 self.execute_command(
442 command=self._command_by_name('get curve'))
444 def _postprocess_add_block_force_array(
445 self, command, args={}, results=[]):
446 """Update the curve, since the available columns may have changed.
448 if isinstance(results[-1], Success):
449 self.execute_command(
450 command=self._command_by_name('get curve'))
456 def _GetActiveFileIndex(self):
457 lib.playlist.Playlist = self.GetActivePlaylist()
458 #get the selected item from the tree
459 selected_item = self._c['playlist']._c['tree'].GetSelection()
460 #test if a playlist or a curve was double-clicked
461 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
465 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
466 while selected_item.IsOk():
468 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
471 def _GetPlaylistTab(self, name):
472 for index, page in enumerate(self._c['notebook']._tabs._pages):
473 if page.caption == name:
477 def select_plugin(self, _class=None, method=None, plugin=None):
480 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
482 playlist = lib.playlist.Playlist(self, self.drivers)
484 playlist.add_curve(item)
485 if playlist.count > 0:
486 playlist.name = self._GetUniquePlaylistName(name)
488 self.AddTayliss(playlist)
490 def AppliesPlotmanipulator(self, name):
492 Returns True if the plotmanipulator 'name' is applied, False otherwise
493 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
495 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
497 def ApplyPlotmanipulators(self, plot, plot_file):
499 Apply all active plotmanipulators.
501 if plot is not None and plot_file is not None:
502 manipulated_plot = copy.deepcopy(plot)
503 for plotmanipulator in self.plotmanipulators:
504 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
505 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
506 return manipulated_plot
508 def GetActiveFigure(self):
509 playlist_name = self.GetActivePlaylistName()
510 figure = self.playlists[playlist_name].figure
511 if figure is not None:
515 def GetActiveFile(self):
516 playlist = self.GetActivePlaylist()
517 if playlist is not None:
518 return playlist.get_active_file()
521 def GetActivePlot(self):
522 playlist = self.GetActivePlaylist()
523 if playlist is not None:
524 return playlist.get_active_file().plot
527 def GetDisplayedPlot(self):
528 plot = copy.deepcopy(self.displayed_plot)
530 #plot.curves = copy.deepcopy(plot.curves)
533 def GetDisplayedPlotCorrected(self):
534 plot = copy.deepcopy(self.displayed_plot)
536 plot.curves = copy.deepcopy(plot.corrected_curves)
539 def GetDisplayedPlotRaw(self):
540 plot = copy.deepcopy(self.displayed_plot)
542 plot.curves = copy.deepcopy(plot.raw_curves)
545 def GetDockArt(self):
546 return self._c['manager'].GetArtProvider()
548 def GetPlotmanipulator(self, name):
550 Returns a plot manipulator function from its name
552 for plotmanipulator in self.plotmanipulators:
553 if plotmanipulator.name == name:
554 return plotmanipulator
557 def HasPlotmanipulator(self, name):
559 returns True if the plotmanipulator 'name' is loaded, False otherwise
561 for plotmanipulator in self.plotmanipulators:
562 if plotmanipulator.command == name:
567 def _on_dir_ctrl_left_double_click(self, event):
568 file_path = self.panelFolders.GetPath()
569 if os.path.isfile(file_path):
570 if file_path.endswith('.hkp'):
571 self.do_loadlist(file_path)
574 def _on_erase_background(self, event):
577 def _on_notebook_page_close(self, event):
578 ctrl = event.GetEventObject()
579 playlist_name = ctrl.GetPageText(ctrl._curpage)
580 self.DeleteFromPlaylists(playlist_name)
582 def OnPaneClose(self, event):
585 def OnPropGridChanged (self, event):
586 prop = event.GetProperty()
588 item_section = self.panelProperties.SelectedTreeItem
589 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
590 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
591 config = self.gui.config[plugin]
592 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
593 property_key = prop.GetName()
594 property_value = prop.GetDisplayedString()
596 config[property_section][property_key]['value'] = property_value
598 def OnResultsCheck(self, index, flag):
599 results = self.GetActivePlot().results
600 if results.has_key(self.results_str):
601 results[self.results_str].results[index].visible = flag
602 results[self.results_str].update()
606 def _on_size(self, event):
609 def UpdatePlaylistsTreeSelection(self):
610 playlist = self.GetActivePlaylist()
611 if playlist is not None:
612 if playlist.index >= 0:
613 self._c['status bar'].set_playlist(playlist)
617 def _on_curve_select(self, playlist, curve):
618 #create the plot tab and add playlist to the dictionary
619 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
620 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
621 #tab_index = self._c['notebook'].GetSelection()
622 playlist.figure = plotPanel.get_figure()
623 self.playlists[playlist.name] = playlist
624 #self.playlists[playlist.name] = [playlist, figure]
625 self._c['status bar'].set_playlist(playlist)
630 def _on_playlist_left_doubleclick(self):
631 index = self._c['notebook'].GetSelection()
632 current_playlist = self._c['notebook'].GetPageText(index)
633 if current_playlist != playlist_name:
634 index = self._GetPlaylistTab(playlist_name)
635 self._c['notebook'].SetSelection(index)
636 self._c['status bar'].set_playlist(playlist)
640 def _on_playlist_delete(self, playlist):
641 notebook = self.Parent.plotNotebook
642 index = self.Parent._GetPlaylistTab(playlist.name)
643 notebook.SetSelection(index)
644 notebook.DeletePage(notebook.GetSelection())
645 self.Parent.DeleteFromPlaylists(playlist_name)
649 # Command panel interface
651 def select_command(self, _class, method, command):
652 #self.select_plugin(plugin=command.plugin)
653 if 'assistant' in self._c:
654 self._c['assitant'].ChangeValue(command.help)
655 self._c['property editor'].clear()
656 for argument in command.arguments:
657 if argument.name == 'help':
660 results = self.execute_command(
661 command=self._command_by_name('playlists'))
662 if not isinstance(results[-1], Success):
663 self._postprocess_text(command, results=results)
666 playlists = results[0]
668 results = self.execute_command(
669 command=self._command_by_name('playlist curves'))
670 if not isinstance(results[-1], Success):
671 self._postprocess_text(command, results=results)
676 p = prop_from_argument(
677 argument, curves=curves, playlists=playlists)
679 continue # property intentionally not handled (yet)
680 self._c['property editor'].append_property(p)
682 self.gui.config['selected command'] = command # TODO: push to engine
686 # Note panel interface
688 def _on_update_note(self, _class, method, text):
689 """Sets the note for the active curve.
691 # TODO: note list interface in NotePanel.
692 self.execute_command(
693 command=self._command_by_name('set note'),
698 # Playlist panel interface
700 def _on_user_delete_playlist(self, _class, method, playlist):
703 def _on_delete_playlist(self, _class, method, playlist):
704 if hasattr(playlist, 'path') and playlist.path != None:
705 os.remove(playlist.path)
707 def _on_user_delete_curve(self, _class, method, playlist, curve):
710 def _on_delete_curve(self, _class, method, playlist, curve):
711 os.remove(curve.path)
713 def _on_set_selected_playlist(self, _class, method, playlist):
714 """Call the `jump to playlist` command.
716 results = self.execute_command(
717 command=self._command_by_name('playlists'))
718 if not isinstance(results[-1], Success):
720 assert len(results) == 2, results
721 playlists = results[0]
722 matching = [p for p in playlists if p.name == playlist.name]
723 assert len(matching) == 1, matching
724 index = playlists.index(matching[0])
725 results = self.execute_command(
726 command=self._command_by_name('jump to playlist'),
727 args={'index':index})
729 def _on_set_selected_curve(self, _class, method, playlist, curve):
730 """Call the `jump to curve` command.
732 TODO: playlists plugin.
734 self._on_set_selected_playlist(_class, method, playlist)
735 index = playlist.index(curve)
736 results = self.execute_command(
737 command=self._command_by_name('jump to curve'),
738 args={'index':index})
739 if not isinstance(results[-1], Success):
741 #results = self.execute_command(
742 # command=self._command_by_name('get playlist'))
743 #if not isinstance(results[-1], Success):
745 self.execute_command(
746 command=self._command_by_name('get curve'))
752 def _next_curve(self, *args):
753 """Call the `next curve` command.
755 results = self.execute_command(
756 command=self._command_by_name('next curve'))
757 if isinstance(results[-1], Success):
758 self.execute_command(
759 command=self._command_by_name('get curve'))
761 def _previous_curve(self, *args):
762 """Call the `previous curve` command.
764 results = self.execute_command(
765 command=self._command_by_name('previous curve'))
766 if isinstance(results[-1], Success):
767 self.execute_command(
768 command=self._command_by_name('get curve'))
772 # Panel display handling
774 def _on_panel_visibility(self, _class, method, panel_name, visible):
775 pane = self._c['manager'].GetPane(panel_name)
777 #if we don't do the following, the Folders pane does not resize properly on hide/show
778 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
779 #folders_size = pane.GetSize()
780 self.panelFolders.Fit()
781 self._c['manager'].Update()
783 def _setup_perspectives(self):
784 """Add perspectives to menubar and _perspectives.
786 self._perspectives = {
787 'Default': self._c['manager'].SavePerspective(),
789 path = self.gui.config['perspective path']
790 if os.path.isdir(path):
791 files = sorted(os.listdir(path))
793 name, extension = os.path.splitext(fname)
794 if extension != self.gui.config['perspective extension']:
796 fpath = os.path.join(path, fname)
797 if not os.path.isfile(fpath):
800 with open(fpath, 'rU') as f:
801 perspective = f.readline()
803 self._perspectives[name] = perspective
805 selected_perspective = self.gui.config['active perspective']
806 if not self._perspectives.has_key(selected_perspective):
807 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
809 self._restore_perspective(selected_perspective, force=True)
810 self._update_perspective_menu()
812 def _update_perspective_menu(self):
813 self._c['menu bar']._c['perspective'].update(
814 sorted(self._perspectives.keys()),
815 self.gui.config['active perspective'])
817 def _save_perspective(self, perspective, perspective_dir, name,
819 path = os.path.join(perspective_dir, name)
820 if extension != None:
822 if not os.path.isdir(perspective_dir):
823 os.makedirs(perspective_dir)
824 with open(path, 'w') as f:
826 self._perspectives[name] = perspective
827 self._restore_perspective(name)
828 self._update_perspective_menu()
830 def _delete_perspectives(self, perspective_dir, names,
832 self.log.debug('remove perspectives %s from %s'
833 % (names, perspective_dir))
835 path = os.path.join(perspective_dir, name)
836 if extension != None:
839 del(self._perspectives[name])
840 self._update_perspective_menu()
841 if self.gui.config['active perspective'] in names:
842 self._restore_perspective('Default')
843 # TODO: does this bug still apply?
844 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
845 # http://trac.wxwidgets.org/ticket/3258
846 # ) that makes the radio item indicator in the menu disappear.
847 # The code should be fine once this issue is fixed.
849 def _restore_perspective(self, name, force=False):
850 if name != self.gui.config['active perspective'] or force == True:
851 self.log.debug('restore perspective %s' % name)
852 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
853 self._c['manager'].LoadPerspective(self._perspectives[name])
854 self._c['manager'].Update()
855 for pane in self._c['manager'].GetAllPanes():
856 view = self._c['menu bar']._c['view']
857 if pane.name in view._c.keys():
858 view._c[pane.name].Check(pane.window.IsShown())
860 def _on_save_perspective(self, *args):
861 perspective = self._c['manager'].SavePerspective()
862 name = self.gui.config['active perspective']
863 if name == 'Default':
864 name = 'New perspective'
865 name = select_save_file(
866 directory=self.gui.config['perspective path'],
868 extension=self.gui.config['perspective extension'],
870 message='Enter a name for the new perspective:',
871 caption='Save perspective')
874 self._save_perspective(
875 perspective, self.gui.config['perspective path'], name=name,
876 extension=self.gui.config['perspective extension'])
878 def _on_delete_perspective(self, *args, **kwargs):
879 options = sorted([p for p in self._perspectives.keys()
881 dialog = SelectionDialog(
883 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
884 button_id=wx.ID_DELETE,
885 selection_style='multiple',
887 title='Delete perspective(s)',
888 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
889 dialog.CenterOnScreen()
891 names = [options[i] for i in dialog.selected]
893 self._delete_perspectives(
894 self.gui.config['perspective path'], names=names,
895 extension=self.gui.config['perspective extension'])
897 def _on_select_perspective(self, _class, method, name):
898 self._restore_perspective(name)
902 class HookeApp (wx.App):
903 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
905 Tosses up a splash screen and then loads :class:`HookeFrame` in
908 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
910 self.commands = commands
911 self.inqueue = inqueue
912 self.outqueue = outqueue
913 super(HookeApp, self).__init__(*args, **kwargs)
916 self.SetAppName('Hooke')
917 self.SetVendorName('')
918 self._setup_splash_screen()
920 height = int(self.gui.config['main height']) # HACK: config should convert
921 width = int(self.gui.config['main width'])
922 top = int(self.gui.config['main top'])
923 left = int(self.gui.config['main left'])
925 # Sometimes, the ini file gets confused and sets 'left' and
926 # 'top' to large negative numbers. Here we catch and fix
927 # this. Keep small negative numbers, the user might want
936 self.gui, self.commands, self.inqueue, self.outqueue,
937 parent=None, title='Hooke',
938 pos=(left, top), size=(width, height),
939 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
941 self._c['frame'].Show(True)
942 self.SetTopWindow(self._c['frame'])
945 def _setup_splash_screen(self):
946 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
947 path = self.gui.config['splash screen image']
948 if os.path.isfile(path):
949 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
951 bitmap=wx.Image(path).ConvertToBitmap(),
952 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
953 milliseconds=duration,
956 # For some reason splashDuration and sleep do not
957 # correspond to each other at least not on Windows.
958 # Maybe it's because duration is in milliseconds and
959 # sleep in seconds. Thus we need to increase the
960 # sleep time a bit. A factor of 1.2 seems to work.
962 time.sleep(sleepFactor * duration / 1000)
965 class GUI (UserInterface):
966 """wxWindows graphical user interface.
969 super(GUI, self).__init__(name='gui')
971 def default_settings(self):
972 """Return a list of :class:`hooke.config.Setting`\s for any
973 configurable UI settings.
975 The suggested section setting is::
977 Setting(section=self.setting_section, help=self.__doc__)
980 Setting(section=self.setting_section, help=self.__doc__),
981 Setting(section=self.setting_section, option='icon image',
982 value=os.path.join('doc', 'img', 'microscope.ico'),
983 help='Path to the hooke icon image.'),
984 Setting(section=self.setting_section, option='show splash screen',
986 help='Enable/disable the splash screen'),
987 Setting(section=self.setting_section, option='splash screen image',
988 value=os.path.join('doc', 'img', 'hooke.jpg'),
989 help='Path to the Hooke splash screen image.'),
990 Setting(section=self.setting_section, option='splash screen duration',
992 help='Duration of the splash screen in milliseconds.'),
993 Setting(section=self.setting_section, option='perspective path',
994 value=os.path.join('resources', 'gui', 'perspective'),
995 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
996 Setting(section=self.setting_section, option='perspective extension',
998 help='Extension for perspective files.'),
999 Setting(section=self.setting_section, option='hide extensions',
1001 help='Hide file extensions when displaying names.'),
1002 Setting(section=self.setting_section, option='plot legend',
1004 help='Enable/disable the plot legend.'),
1005 Setting(section=self.setting_section, option='plot SI format',
1007 help='Enable/disable SI plot axes numbering.'),
1008 Setting(section=self.setting_section, option='plot decimals',
1010 help='Number of decimal places to show if "plot SI format" is enabled.'),
1011 Setting(section=self.setting_section, option='folders-workdir',
1013 help='This should probably go...'),
1014 Setting(section=self.setting_section, option='folders-filters',
1016 help='This should probably go...'),
1017 Setting(section=self.setting_section, option='active perspective',
1019 help='Name of active perspective file (or "Default").'),
1020 Setting(section=self.setting_section, option='folders-filter-index',
1022 help='This should probably go...'),
1023 Setting(section=self.setting_section, option='main height',
1025 help='Height of main window in pixels.'),
1026 Setting(section=self.setting_section, option='main width',
1028 help='Width of main window in pixels.'),
1029 Setting(section=self.setting_section, option='main top',
1031 help='Pixels from screen top to top of main window.'),
1032 Setting(section=self.setting_section, option='main left',
1034 help='Pixels from screen left to left of main window.'),
1035 Setting(section=self.setting_section, option='selected command',
1036 value='load playlist',
1037 help='Name of the initially selected command.'),
1040 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1044 app = HookeApp(gui=self,
1046 inqueue=ui_to_command_queue,
1047 outqueue=command_to_ui_queue,
1051 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1052 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)