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 print sorted(curve.info.keys())
420 self._c['note'].set_text(curve.info['note'])
421 if 'playlist' in self._c:
422 self._c['playlist']._c['tree'].set_selected_curve(
424 if 'plot' in self._c:
425 self._c['plot'].set_curve(curve, config=self.gui.config)
427 def _postprocess_next_curve(self, command, args={}, results=[]):
428 """No-op. Only call 'next curve' via `self._next_curve()`.
432 def _postprocess_previous_curve(self, command, args={}, results=[]):
433 """No-op. Only call 'previous curve' via `self._previous_curve()`.
437 def _postprocess_zero_block_surface_contact_point(
438 self, command, args={}, results=[]):
439 """Update the curve, since the available columns may have changed.
441 if isinstance(results[-1], Success):
442 self.execute_command(
443 command=self._command_by_name('get curve'))
445 def _postprocess_add_block_force_array(
446 self, command, args={}, results=[]):
447 """Update the curve, since the available columns may have changed.
449 if isinstance(results[-1], Success):
450 self.execute_command(
451 command=self._command_by_name('get curve'))
457 def _GetActiveFileIndex(self):
458 lib.playlist.Playlist = self.GetActivePlaylist()
459 #get the selected item from the tree
460 selected_item = self._c['playlist']._c['tree'].GetSelection()
461 #test if a playlist or a curve was double-clicked
462 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
466 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
467 while selected_item.IsOk():
469 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
472 def _GetPlaylistTab(self, name):
473 for index, page in enumerate(self._c['notebook']._tabs._pages):
474 if page.caption == name:
478 def select_plugin(self, _class=None, method=None, plugin=None):
481 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
483 playlist = lib.playlist.Playlist(self, self.drivers)
485 playlist.add_curve(item)
486 if playlist.count > 0:
487 playlist.name = self._GetUniquePlaylistName(name)
489 self.AddTayliss(playlist)
491 def AppliesPlotmanipulator(self, name):
493 Returns True if the plotmanipulator 'name' is applied, False otherwise
494 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
496 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
498 def ApplyPlotmanipulators(self, plot, plot_file):
500 Apply all active plotmanipulators.
502 if plot is not None and plot_file is not None:
503 manipulated_plot = copy.deepcopy(plot)
504 for plotmanipulator in self.plotmanipulators:
505 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
506 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
507 return manipulated_plot
509 def GetActiveFigure(self):
510 playlist_name = self.GetActivePlaylistName()
511 figure = self.playlists[playlist_name].figure
512 if figure is not None:
516 def GetActiveFile(self):
517 playlist = self.GetActivePlaylist()
518 if playlist is not None:
519 return playlist.get_active_file()
522 def GetActivePlot(self):
523 playlist = self.GetActivePlaylist()
524 if playlist is not None:
525 return playlist.get_active_file().plot
528 def GetDisplayedPlot(self):
529 plot = copy.deepcopy(self.displayed_plot)
531 #plot.curves = copy.deepcopy(plot.curves)
534 def GetDisplayedPlotCorrected(self):
535 plot = copy.deepcopy(self.displayed_plot)
537 plot.curves = copy.deepcopy(plot.corrected_curves)
540 def GetDisplayedPlotRaw(self):
541 plot = copy.deepcopy(self.displayed_plot)
543 plot.curves = copy.deepcopy(plot.raw_curves)
546 def GetDockArt(self):
547 return self._c['manager'].GetArtProvider()
549 def GetPlotmanipulator(self, name):
551 Returns a plot manipulator function from its name
553 for plotmanipulator in self.plotmanipulators:
554 if plotmanipulator.name == name:
555 return plotmanipulator
558 def HasPlotmanipulator(self, name):
560 returns True if the plotmanipulator 'name' is loaded, False otherwise
562 for plotmanipulator in self.plotmanipulators:
563 if plotmanipulator.command == name:
568 def _on_dir_ctrl_left_double_click(self, event):
569 file_path = self.panelFolders.GetPath()
570 if os.path.isfile(file_path):
571 if file_path.endswith('.hkp'):
572 self.do_loadlist(file_path)
575 def _on_erase_background(self, event):
578 def _on_notebook_page_close(self, event):
579 ctrl = event.GetEventObject()
580 playlist_name = ctrl.GetPageText(ctrl._curpage)
581 self.DeleteFromPlaylists(playlist_name)
583 def OnPaneClose(self, event):
586 def OnPropGridChanged (self, event):
587 prop = event.GetProperty()
589 item_section = self.panelProperties.SelectedTreeItem
590 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
591 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
592 config = self.gui.config[plugin]
593 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
594 property_key = prop.GetName()
595 property_value = prop.GetDisplayedString()
597 config[property_section][property_key]['value'] = property_value
599 def OnResultsCheck(self, index, flag):
600 results = self.GetActivePlot().results
601 if results.has_key(self.results_str):
602 results[self.results_str].results[index].visible = flag
603 results[self.results_str].update()
607 def _on_size(self, event):
610 def UpdatePlaylistsTreeSelection(self):
611 playlist = self.GetActivePlaylist()
612 if playlist is not None:
613 if playlist.index >= 0:
614 self._c['status bar'].set_playlist(playlist)
618 def _on_curve_select(self, playlist, curve):
619 #create the plot tab and add playlist to the dictionary
620 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
621 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
622 #tab_index = self._c['notebook'].GetSelection()
623 playlist.figure = plotPanel.get_figure()
624 self.playlists[playlist.name] = playlist
625 #self.playlists[playlist.name] = [playlist, figure]
626 self._c['status bar'].set_playlist(playlist)
631 def _on_playlist_left_doubleclick(self):
632 index = self._c['notebook'].GetSelection()
633 current_playlist = self._c['notebook'].GetPageText(index)
634 if current_playlist != playlist_name:
635 index = self._GetPlaylistTab(playlist_name)
636 self._c['notebook'].SetSelection(index)
637 self._c['status bar'].set_playlist(playlist)
641 def _on_playlist_delete(self, playlist):
642 notebook = self.Parent.plotNotebook
643 index = self.Parent._GetPlaylistTab(playlist.name)
644 notebook.SetSelection(index)
645 notebook.DeletePage(notebook.GetSelection())
646 self.Parent.DeleteFromPlaylists(playlist_name)
650 # Command panel interface
652 def select_command(self, _class, method, command):
653 #self.select_plugin(plugin=command.plugin)
654 if 'assistant' in self._c:
655 self._c['assitant'].ChangeValue(command.help)
656 self._c['property editor'].clear()
657 for argument in command.arguments:
658 if argument.name == 'help':
661 results = self.execute_command(
662 command=self._command_by_name('playlists'))
663 if not isinstance(results[-1], Success):
664 self._postprocess_text(command, results=results)
667 playlists = results[0]
669 results = self.execute_command(
670 command=self._command_by_name('playlist curves'))
671 if not isinstance(results[-1], Success):
672 self._postprocess_text(command, results=results)
677 p = prop_from_argument(
678 argument, curves=curves, playlists=playlists)
680 continue # property intentionally not handled (yet)
681 self._c['property editor'].append_property(p)
683 self.gui.config['selected command'] = command # TODO: push to engine
687 # Note panel interface
689 def _on_update_note(self, _class, method, text):
690 """Sets the note for the active curve.
692 # TODO: note list interface in NotePanel.
693 self.execute_command(
694 command=self._command_by_name('set note'),
699 # Playlist panel interface
701 def _on_user_delete_playlist(self, _class, method, playlist):
704 def _on_delete_playlist(self, _class, method, playlist):
705 if hasattr(playlist, 'path') and playlist.path != None:
706 os.remove(playlist.path)
708 def _on_user_delete_curve(self, _class, method, playlist, curve):
711 def _on_delete_curve(self, _class, method, playlist, curve):
712 os.remove(curve.path)
714 def _on_set_selected_playlist(self, _class, method, playlist):
715 """TODO: playlists plugin with `jump to playlist`.
719 def _on_set_selected_curve(self, _class, method, playlist, curve):
720 """Call the `jump to curve` command.
722 TODO: playlists plugin.
724 # TODO: jump to playlist, get playlist
725 index = playlist.index(curve)
726 results = self.execute_command(
727 command=self._command_by_name('jump to curve'),
728 args={'index':index})
729 if not isinstance(results[-1], Success):
731 #results = self.execute_command(
732 # command=self._command_by_name('get playlist'))
733 #if not isinstance(results[-1], Success):
735 self.execute_command(
736 command=self._command_by_name('get curve'))
742 def _next_curve(self, *args):
743 """Call the `next curve` command.
745 results = self.execute_command(
746 command=self._command_by_name('next curve'))
747 if isinstance(results[-1], Success):
748 self.execute_command(
749 command=self._command_by_name('get curve'))
751 def _previous_curve(self, *args):
752 """Call the `previous curve` command.
754 results = self.execute_command(
755 command=self._command_by_name('previous curve'))
756 if isinstance(results[-1], Success):
757 self.execute_command(
758 command=self._command_by_name('get curve'))
762 # Panel display handling
764 def _on_panel_visibility(self, _class, method, panel_name, visible):
765 pane = self._c['manager'].GetPane(panel_name)
767 #if we don't do the following, the Folders pane does not resize properly on hide/show
768 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
769 #folders_size = pane.GetSize()
770 self.panelFolders.Fit()
771 self._c['manager'].Update()
773 def _setup_perspectives(self):
774 """Add perspectives to menubar and _perspectives.
776 self._perspectives = {
777 'Default': self._c['manager'].SavePerspective(),
779 path = self.gui.config['perspective path']
780 if os.path.isdir(path):
781 files = sorted(os.listdir(path))
783 name, extension = os.path.splitext(fname)
784 if extension != self.gui.config['perspective extension']:
786 fpath = os.path.join(path, fname)
787 if not os.path.isfile(fpath):
790 with open(fpath, 'rU') as f:
791 perspective = f.readline()
793 self._perspectives[name] = perspective
795 selected_perspective = self.gui.config['active perspective']
796 if not self._perspectives.has_key(selected_perspective):
797 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
799 self._restore_perspective(selected_perspective, force=True)
800 self._update_perspective_menu()
802 def _update_perspective_menu(self):
803 self._c['menu bar']._c['perspective'].update(
804 sorted(self._perspectives.keys()),
805 self.gui.config['active perspective'])
807 def _save_perspective(self, perspective, perspective_dir, name,
809 path = os.path.join(perspective_dir, name)
810 if extension != None:
812 if not os.path.isdir(perspective_dir):
813 os.makedirs(perspective_dir)
814 with open(path, 'w') as f:
816 self._perspectives[name] = perspective
817 self._restore_perspective(name)
818 self._update_perspective_menu()
820 def _delete_perspectives(self, perspective_dir, names,
822 self.log.debug('remove perspectives %s from %s'
823 % (names, perspective_dir))
825 path = os.path.join(perspective_dir, name)
826 if extension != None:
829 del(self._perspectives[name])
830 self._update_perspective_menu()
831 if self.gui.config['active perspective'] in names:
832 self._restore_perspective('Default')
833 # TODO: does this bug still apply?
834 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
835 # http://trac.wxwidgets.org/ticket/3258
836 # ) that makes the radio item indicator in the menu disappear.
837 # The code should be fine once this issue is fixed.
839 def _restore_perspective(self, name, force=False):
840 if name != self.gui.config['active perspective'] or force == True:
841 self.log.debug('restore perspective %s' % name)
842 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
843 self._c['manager'].LoadPerspective(self._perspectives[name])
844 self._c['manager'].Update()
845 for pane in self._c['manager'].GetAllPanes():
846 view = self._c['menu bar']._c['view']
847 if pane.name in view._c.keys():
848 view._c[pane.name].Check(pane.window.IsShown())
850 def _on_save_perspective(self, *args):
851 perspective = self._c['manager'].SavePerspective()
852 name = self.gui.config['active perspective']
853 if name == 'Default':
854 name = 'New perspective'
855 name = select_save_file(
856 directory=self.gui.config['perspective path'],
858 extension=self.gui.config['perspective extension'],
860 message='Enter a name for the new perspective:',
861 caption='Save perspective')
864 self._save_perspective(
865 perspective, self.gui.config['perspective path'], name=name,
866 extension=self.gui.config['perspective extension'])
868 def _on_delete_perspective(self, *args, **kwargs):
869 options = sorted([p for p in self._perspectives.keys()
871 dialog = SelectionDialog(
873 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
874 button_id=wx.ID_DELETE,
875 selection_style='multiple',
877 title='Delete perspective(s)',
878 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
879 dialog.CenterOnScreen()
881 names = [options[i] for i in dialog.selected]
883 self._delete_perspectives(
884 self.gui.config['perspective path'], names=names,
885 extension=self.gui.config['perspective extension'])
887 def _on_select_perspective(self, _class, method, name):
888 self._restore_perspective(name)
892 class HookeApp (wx.App):
893 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
895 Tosses up a splash screen and then loads :class:`HookeFrame` in
898 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
900 self.commands = commands
901 self.inqueue = inqueue
902 self.outqueue = outqueue
903 super(HookeApp, self).__init__(*args, **kwargs)
906 self.SetAppName('Hooke')
907 self.SetVendorName('')
908 self._setup_splash_screen()
910 height = int(self.gui.config['main height']) # HACK: config should convert
911 width = int(self.gui.config['main width'])
912 top = int(self.gui.config['main top'])
913 left = int(self.gui.config['main left'])
915 # Sometimes, the ini file gets confused and sets 'left' and
916 # 'top' to large negative numbers. Here we catch and fix
917 # this. Keep small negative numbers, the user might want
926 self.gui, self.commands, self.inqueue, self.outqueue,
927 parent=None, title='Hooke',
928 pos=(left, top), size=(width, height),
929 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
931 self._c['frame'].Show(True)
932 self.SetTopWindow(self._c['frame'])
935 def _setup_splash_screen(self):
936 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
937 path = self.gui.config['splash screen image']
938 if os.path.isfile(path):
939 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
941 bitmap=wx.Image(path).ConvertToBitmap(),
942 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
943 milliseconds=duration,
946 # For some reason splashDuration and sleep do not
947 # correspond to each other at least not on Windows.
948 # Maybe it's because duration is in milliseconds and
949 # sleep in seconds. Thus we need to increase the
950 # sleep time a bit. A factor of 1.2 seems to work.
952 time.sleep(sleepFactor * duration / 1000)
955 class GUI (UserInterface):
956 """wxWindows graphical user interface.
959 super(GUI, self).__init__(name='gui')
961 def default_settings(self):
962 """Return a list of :class:`hooke.config.Setting`\s for any
963 configurable UI settings.
965 The suggested section setting is::
967 Setting(section=self.setting_section, help=self.__doc__)
970 Setting(section=self.setting_section, help=self.__doc__),
971 Setting(section=self.setting_section, option='icon image',
972 value=os.path.join('doc', 'img', 'microscope.ico'),
973 help='Path to the hooke icon image.'),
974 Setting(section=self.setting_section, option='show splash screen',
976 help='Enable/disable the splash screen'),
977 Setting(section=self.setting_section, option='splash screen image',
978 value=os.path.join('doc', 'img', 'hooke.jpg'),
979 help='Path to the Hooke splash screen image.'),
980 Setting(section=self.setting_section, option='splash screen duration',
982 help='Duration of the splash screen in milliseconds.'),
983 Setting(section=self.setting_section, option='perspective path',
984 value=os.path.join('resources', 'gui', 'perspective'),
985 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
986 Setting(section=self.setting_section, option='perspective extension',
988 help='Extension for perspective files.'),
989 Setting(section=self.setting_section, option='hide extensions',
991 help='Hide file extensions when displaying names.'),
992 Setting(section=self.setting_section, option='plot legend',
994 help='Enable/disable the plot legend.'),
995 Setting(section=self.setting_section, option='plot SI format',
997 help='Enable/disable SI plot axes numbering.'),
998 Setting(section=self.setting_section, option='plot decimals',
1000 help='Number of decimal places to show if "plot SI format" is enabled.'),
1001 Setting(section=self.setting_section, option='folders-workdir',
1003 help='This should probably go...'),
1004 Setting(section=self.setting_section, option='folders-filters',
1006 help='This should probably go...'),
1007 Setting(section=self.setting_section, option='active perspective',
1009 help='Name of active perspective file (or "Default").'),
1010 Setting(section=self.setting_section, option='folders-filter-index',
1012 help='This should probably go...'),
1013 Setting(section=self.setting_section, option='main height',
1015 help='Height of main window in pixels.'),
1016 Setting(section=self.setting_section, option='main width',
1018 help='Width of main window in pixels.'),
1019 Setting(section=self.setting_section, option='main top',
1021 help='Pixels from screen top to top of main window.'),
1022 Setting(section=self.setting_section, option='main left',
1024 help='Pixels from screen left to left of main window.'),
1025 Setting(section=self.setting_section, option='selected command',
1026 value='load playlist',
1027 help='Name of the initially selected command.'),
1030 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1034 app = HookeApp(gui=self,
1036 inqueue=ui_to_command_queue,
1037 outqueue=command_to_ui_queue,
1041 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1042 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)