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 """TODO: playlists plugin with `jump to playlist`.
718 def _on_set_selected_curve(self, _class, method, playlist, curve):
719 """Call the `jump to curve` command.
721 TODO: playlists plugin.
723 # TODO: jump to playlist, get playlist
724 index = playlist.index(curve)
725 results = self.execute_command(
726 command=self._command_by_name('jump to curve'),
727 args={'index':index})
728 if not isinstance(results[-1], Success):
730 #results = self.execute_command(
731 # command=self._command_by_name('get playlist'))
732 #if not isinstance(results[-1], Success):
734 self.execute_command(
735 command=self._command_by_name('get curve'))
741 def _next_curve(self, *args):
742 """Call the `next curve` command.
744 results = self.execute_command(
745 command=self._command_by_name('next curve'))
746 if isinstance(results[-1], Success):
747 self.execute_command(
748 command=self._command_by_name('get curve'))
750 def _previous_curve(self, *args):
751 """Call the `previous curve` command.
753 results = self.execute_command(
754 command=self._command_by_name('previous curve'))
755 if isinstance(results[-1], Success):
756 self.execute_command(
757 command=self._command_by_name('get curve'))
761 # Panel display handling
763 def _on_panel_visibility(self, _class, method, panel_name, visible):
764 pane = self._c['manager'].GetPane(panel_name)
766 #if we don't do the following, the Folders pane does not resize properly on hide/show
767 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
768 #folders_size = pane.GetSize()
769 self.panelFolders.Fit()
770 self._c['manager'].Update()
772 def _setup_perspectives(self):
773 """Add perspectives to menubar and _perspectives.
775 self._perspectives = {
776 'Default': self._c['manager'].SavePerspective(),
778 path = self.gui.config['perspective path']
779 if os.path.isdir(path):
780 files = sorted(os.listdir(path))
782 name, extension = os.path.splitext(fname)
783 if extension != self.gui.config['perspective extension']:
785 fpath = os.path.join(path, fname)
786 if not os.path.isfile(fpath):
789 with open(fpath, 'rU') as f:
790 perspective = f.readline()
792 self._perspectives[name] = perspective
794 selected_perspective = self.gui.config['active perspective']
795 if not self._perspectives.has_key(selected_perspective):
796 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
798 self._restore_perspective(selected_perspective, force=True)
799 self._update_perspective_menu()
801 def _update_perspective_menu(self):
802 self._c['menu bar']._c['perspective'].update(
803 sorted(self._perspectives.keys()),
804 self.gui.config['active perspective'])
806 def _save_perspective(self, perspective, perspective_dir, name,
808 path = os.path.join(perspective_dir, name)
809 if extension != None:
811 if not os.path.isdir(perspective_dir):
812 os.makedirs(perspective_dir)
813 with open(path, 'w') as f:
815 self._perspectives[name] = perspective
816 self._restore_perspective(name)
817 self._update_perspective_menu()
819 def _delete_perspectives(self, perspective_dir, names,
821 self.log.debug('remove perspectives %s from %s'
822 % (names, perspective_dir))
824 path = os.path.join(perspective_dir, name)
825 if extension != None:
828 del(self._perspectives[name])
829 self._update_perspective_menu()
830 if self.gui.config['active perspective'] in names:
831 self._restore_perspective('Default')
832 # TODO: does this bug still apply?
833 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
834 # http://trac.wxwidgets.org/ticket/3258
835 # ) that makes the radio item indicator in the menu disappear.
836 # The code should be fine once this issue is fixed.
838 def _restore_perspective(self, name, force=False):
839 if name != self.gui.config['active perspective'] or force == True:
840 self.log.debug('restore perspective %s' % name)
841 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
842 self._c['manager'].LoadPerspective(self._perspectives[name])
843 self._c['manager'].Update()
844 for pane in self._c['manager'].GetAllPanes():
845 view = self._c['menu bar']._c['view']
846 if pane.name in view._c.keys():
847 view._c[pane.name].Check(pane.window.IsShown())
849 def _on_save_perspective(self, *args):
850 perspective = self._c['manager'].SavePerspective()
851 name = self.gui.config['active perspective']
852 if name == 'Default':
853 name = 'New perspective'
854 name = select_save_file(
855 directory=self.gui.config['perspective path'],
857 extension=self.gui.config['perspective extension'],
859 message='Enter a name for the new perspective:',
860 caption='Save perspective')
863 self._save_perspective(
864 perspective, self.gui.config['perspective path'], name=name,
865 extension=self.gui.config['perspective extension'])
867 def _on_delete_perspective(self, *args, **kwargs):
868 options = sorted([p for p in self._perspectives.keys()
870 dialog = SelectionDialog(
872 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
873 button_id=wx.ID_DELETE,
874 selection_style='multiple',
876 title='Delete perspective(s)',
877 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
878 dialog.CenterOnScreen()
880 names = [options[i] for i in dialog.selected]
882 self._delete_perspectives(
883 self.gui.config['perspective path'], names=names,
884 extension=self.gui.config['perspective extension'])
886 def _on_select_perspective(self, _class, method, name):
887 self._restore_perspective(name)
891 class HookeApp (wx.App):
892 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
894 Tosses up a splash screen and then loads :class:`HookeFrame` in
897 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
899 self.commands = commands
900 self.inqueue = inqueue
901 self.outqueue = outqueue
902 super(HookeApp, self).__init__(*args, **kwargs)
905 self.SetAppName('Hooke')
906 self.SetVendorName('')
907 self._setup_splash_screen()
909 height = int(self.gui.config['main height']) # HACK: config should convert
910 width = int(self.gui.config['main width'])
911 top = int(self.gui.config['main top'])
912 left = int(self.gui.config['main left'])
914 # Sometimes, the ini file gets confused and sets 'left' and
915 # 'top' to large negative numbers. Here we catch and fix
916 # this. Keep small negative numbers, the user might want
925 self.gui, self.commands, self.inqueue, self.outqueue,
926 parent=None, title='Hooke',
927 pos=(left, top), size=(width, height),
928 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
930 self._c['frame'].Show(True)
931 self.SetTopWindow(self._c['frame'])
934 def _setup_splash_screen(self):
935 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
936 path = self.gui.config['splash screen image']
937 if os.path.isfile(path):
938 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
940 bitmap=wx.Image(path).ConvertToBitmap(),
941 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
942 milliseconds=duration,
945 # For some reason splashDuration and sleep do not
946 # correspond to each other at least not on Windows.
947 # Maybe it's because duration is in milliseconds and
948 # sleep in seconds. Thus we need to increase the
949 # sleep time a bit. A factor of 1.2 seems to work.
951 time.sleep(sleepFactor * duration / 1000)
954 class GUI (UserInterface):
955 """wxWindows graphical user interface.
958 super(GUI, self).__init__(name='gui')
960 def default_settings(self):
961 """Return a list of :class:`hooke.config.Setting`\s for any
962 configurable UI settings.
964 The suggested section setting is::
966 Setting(section=self.setting_section, help=self.__doc__)
969 Setting(section=self.setting_section, help=self.__doc__),
970 Setting(section=self.setting_section, option='icon image',
971 value=os.path.join('doc', 'img', 'microscope.ico'),
972 help='Path to the hooke icon image.'),
973 Setting(section=self.setting_section, option='show splash screen',
975 help='Enable/disable the splash screen'),
976 Setting(section=self.setting_section, option='splash screen image',
977 value=os.path.join('doc', 'img', 'hooke.jpg'),
978 help='Path to the Hooke splash screen image.'),
979 Setting(section=self.setting_section, option='splash screen duration',
981 help='Duration of the splash screen in milliseconds.'),
982 Setting(section=self.setting_section, option='perspective path',
983 value=os.path.join('resources', 'gui', 'perspective'),
984 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
985 Setting(section=self.setting_section, option='perspective extension',
987 help='Extension for perspective files.'),
988 Setting(section=self.setting_section, option='hide extensions',
990 help='Hide file extensions when displaying names.'),
991 Setting(section=self.setting_section, option='plot legend',
993 help='Enable/disable the plot legend.'),
994 Setting(section=self.setting_section, option='plot SI format',
996 help='Enable/disable SI plot axes numbering.'),
997 Setting(section=self.setting_section, option='plot decimals',
999 help='Number of decimal places to show if "plot SI format" is enabled.'),
1000 Setting(section=self.setting_section, option='folders-workdir',
1002 help='This should probably go...'),
1003 Setting(section=self.setting_section, option='folders-filters',
1005 help='This should probably go...'),
1006 Setting(section=self.setting_section, option='active perspective',
1008 help='Name of active perspective file (or "Default").'),
1009 Setting(section=self.setting_section, option='folders-filter-index',
1011 help='This should probably go...'),
1012 Setting(section=self.setting_section, option='main height',
1014 help='Height of main window in pixels.'),
1015 Setting(section=self.setting_section, option='main width',
1017 help='Width of main window in pixels.'),
1018 Setting(section=self.setting_section, option='main top',
1020 help='Pixels from screen top to top of main window.'),
1021 Setting(section=self.setting_section, option='main left',
1023 help='Pixels from screen left to left of main window.'),
1024 Setting(section=self.setting_section, option='selected command',
1025 value='load playlist',
1026 help='Name of the initially selected command.'),
1029 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1033 app = HookeApp(gui=self,
1035 inqueue=ui_to_command_queue,
1036 outqueue=command_to_ui_queue,
1040 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1041 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)