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=self.gui.config['folders-filter-index']), 'left'),
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:
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 self.execute_command(
692 command=self._command_by_name('set note'),
697 # Playlist panel interface
699 def _on_user_delete_playlist(self, _class, method, playlist):
702 def _on_delete_playlist(self, _class, method, playlist):
703 if hasattr(playlist, 'path') and playlist.path != None:
704 os.remove(playlist.path)
706 def _on_user_delete_curve(self, _class, method, playlist, curve):
709 def _on_delete_curve(self, _class, method, playlist, curve):
710 os.remove(curve.path)
712 def _on_set_selected_playlist(self, _class, method, playlist):
713 """Call the `jump to playlist` command.
715 results = self.execute_command(
716 command=self._command_by_name('playlists'))
717 if not isinstance(results[-1], Success):
719 assert len(results) == 2, results
720 playlists = results[0]
721 matching = [p for p in playlists if p.name == playlist.name]
722 assert len(matching) == 1, matching
723 index = playlists.index(matching[0])
724 results = self.execute_command(
725 command=self._command_by_name('jump to playlist'),
726 args={'index':index})
728 def _on_set_selected_curve(self, _class, method, playlist, curve):
729 """Call the `jump to curve` command.
731 self._on_set_selected_playlist(_class, method, playlist)
732 index = playlist.index(curve)
733 results = self.execute_command(
734 command=self._command_by_name('jump to curve'),
735 args={'index':index})
736 if not isinstance(results[-1], Success):
738 #results = self.execute_command(
739 # command=self._command_by_name('get playlist'))
740 #if not isinstance(results[-1], Success):
742 self.execute_command(
743 command=self._command_by_name('get curve'))
749 def _next_curve(self, *args):
750 """Call the `next curve` command.
752 results = self.execute_command(
753 command=self._command_by_name('next curve'))
754 if isinstance(results[-1], Success):
755 self.execute_command(
756 command=self._command_by_name('get curve'))
758 def _previous_curve(self, *args):
759 """Call the `previous curve` command.
761 results = self.execute_command(
762 command=self._command_by_name('previous curve'))
763 if isinstance(results[-1], Success):
764 self.execute_command(
765 command=self._command_by_name('get curve'))
769 # Panel display handling
771 def _on_panel_visibility(self, _class, method, panel_name, visible):
772 pane = self._c['manager'].GetPane(panel_name)
774 #if we don't do the following, the Folders pane does not resize properly on hide/show
775 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
776 #folders_size = pane.GetSize()
777 self.panelFolders.Fit()
778 self._c['manager'].Update()
780 def _setup_perspectives(self):
781 """Add perspectives to menubar and _perspectives.
783 self._perspectives = {
784 'Default': self._c['manager'].SavePerspective(),
786 path = self.gui.config['perspective path']
787 if os.path.isdir(path):
788 files = sorted(os.listdir(path))
790 name, extension = os.path.splitext(fname)
791 if extension != self.gui.config['perspective extension']:
793 fpath = os.path.join(path, fname)
794 if not os.path.isfile(fpath):
797 with open(fpath, 'rU') as f:
798 perspective = f.readline()
800 self._perspectives[name] = perspective
802 selected_perspective = self.gui.config['active perspective']
803 if not self._perspectives.has_key(selected_perspective):
804 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
806 self._restore_perspective(selected_perspective, force=True)
807 self._update_perspective_menu()
809 def _update_perspective_menu(self):
810 self._c['menu bar']._c['perspective'].update(
811 sorted(self._perspectives.keys()),
812 self.gui.config['active perspective'])
814 def _save_perspective(self, perspective, perspective_dir, name,
816 path = os.path.join(perspective_dir, name)
817 if extension != None:
819 if not os.path.isdir(perspective_dir):
820 os.makedirs(perspective_dir)
821 with open(path, 'w') as f:
823 self._perspectives[name] = perspective
824 self._restore_perspective(name)
825 self._update_perspective_menu()
827 def _delete_perspectives(self, perspective_dir, names,
829 self.log.debug('remove perspectives %s from %s'
830 % (names, perspective_dir))
832 path = os.path.join(perspective_dir, name)
833 if extension != None:
836 del(self._perspectives[name])
837 self._update_perspective_menu()
838 if self.gui.config['active perspective'] in names:
839 self._restore_perspective('Default')
840 # TODO: does this bug still apply?
841 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
842 # http://trac.wxwidgets.org/ticket/3258
843 # ) that makes the radio item indicator in the menu disappear.
844 # The code should be fine once this issue is fixed.
846 def _restore_perspective(self, name, force=False):
847 if name != self.gui.config['active perspective'] or force == True:
848 self.log.debug('restore perspective %s' % name)
849 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
850 self._c['manager'].LoadPerspective(self._perspectives[name])
851 self._c['manager'].Update()
852 for pane in self._c['manager'].GetAllPanes():
853 view = self._c['menu bar']._c['view']
854 if pane.name in view._c.keys():
855 view._c[pane.name].Check(pane.window.IsShown())
857 def _on_save_perspective(self, *args):
858 perspective = self._c['manager'].SavePerspective()
859 name = self.gui.config['active perspective']
860 if name == 'Default':
861 name = 'New perspective'
862 name = select_save_file(
863 directory=self.gui.config['perspective path'],
865 extension=self.gui.config['perspective extension'],
867 message='Enter a name for the new perspective:',
868 caption='Save perspective')
871 self._save_perspective(
872 perspective, self.gui.config['perspective path'], name=name,
873 extension=self.gui.config['perspective extension'])
875 def _on_delete_perspective(self, *args, **kwargs):
876 options = sorted([p for p in self._perspectives.keys()
878 dialog = SelectionDialog(
880 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
881 button_id=wx.ID_DELETE,
882 selection_style='multiple',
884 title='Delete perspective(s)',
885 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
886 dialog.CenterOnScreen()
888 names = [options[i] for i in dialog.selected]
890 self._delete_perspectives(
891 self.gui.config['perspective path'], names=names,
892 extension=self.gui.config['perspective extension'])
894 def _on_select_perspective(self, _class, method, name):
895 self._restore_perspective(name)
899 class HookeApp (wx.App):
900 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
902 Tosses up a splash screen and then loads :class:`HookeFrame` in
905 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
907 self.commands = commands
908 self.inqueue = inqueue
909 self.outqueue = outqueue
910 super(HookeApp, self).__init__(*args, **kwargs)
913 self.SetAppName('Hooke')
914 self.SetVendorName('')
915 self._setup_splash_screen()
917 height = self.gui.config['main height']
918 width = self.gui.config['main width']
919 top = self.gui.config['main top']
920 left = self.gui.config['main left']
922 # Sometimes, the ini file gets confused and sets 'left' and
923 # 'top' to large negative numbers. Here we catch and fix
924 # this. Keep small negative numbers, the user might want
933 self.gui, self.commands, self.inqueue, self.outqueue,
934 parent=None, title='Hooke',
935 pos=(left, top), size=(width, height),
936 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
938 self._c['frame'].Show(True)
939 self.SetTopWindow(self._c['frame'])
942 def _setup_splash_screen(self):
943 if self.gui.config['show splash screen'] == True:
944 path = self.gui.config['splash screen image']
945 if os.path.isfile(path):
946 duration = self.gui.config['splash screen duration']
948 bitmap=wx.Image(path).ConvertToBitmap(),
949 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
950 milliseconds=duration,
953 # For some reason splashDuration and sleep do not
954 # correspond to each other at least not on Windows.
955 # Maybe it's because duration is in milliseconds and
956 # sleep in seconds. Thus we need to increase the
957 # sleep time a bit. A factor of 1.2 seems to work.
959 time.sleep(sleepFactor * duration / 1000)
962 class GUI (UserInterface):
963 """wxWindows graphical user interface.
966 super(GUI, self).__init__(name='gui')
968 def default_settings(self):
969 """Return a list of :class:`hooke.config.Setting`\s for any
970 configurable UI settings.
972 The suggested section setting is::
974 Setting(section=self.setting_section, help=self.__doc__)
977 Setting(section=self.setting_section, help=self.__doc__),
978 Setting(section=self.setting_section, option='icon image',
979 value=os.path.join('doc', 'img', 'microscope.ico'),
981 help='Path to the hooke icon image.'),
982 Setting(section=self.setting_section, option='show splash screen',
983 value=True, type='bool',
984 help='Enable/disable the splash screen'),
985 Setting(section=self.setting_section, option='splash screen image',
986 value=os.path.join('doc', 'img', 'hooke.jpg'),
988 help='Path to the Hooke splash screen image.'),
989 Setting(section=self.setting_section,
990 option='splash screen duration',
991 value=1000, type='int',
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',
1000 value=False, type='bool',
1001 help='Hide file extensions when displaying names.'),
1002 Setting(section=self.setting_section, option='plot legend',
1003 value=True, type='bool',
1004 help='Enable/disable the plot legend.'),
1005 Setting(section=self.setting_section, option='plot SI format',
1006 value='True', type='bool',
1007 help='Enable/disable SI plot axes numbering.'),
1008 Setting(section=self.setting_section, option='plot decimals',
1009 value=2, type='int',
1010 help='Number of decimal places to show if "plot SI format" is enabled.'),
1011 Setting(section=self.setting_section, option='folders-workdir',
1012 value='.', type='path',
1013 help='This should probably go...'),
1014 Setting(section=self.setting_section, option='folders-filters',
1015 value='.', type='path',
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,
1021 option='folders-filter-index',
1022 value=0, type='int',
1023 help='This should probably go...'),
1024 Setting(section=self.setting_section, option='main height',
1025 value=450, type='int',
1026 help='Height of main window in pixels.'),
1027 Setting(section=self.setting_section, option='main width',
1028 value=800, type='int',
1029 help='Width of main window in pixels.'),
1030 Setting(section=self.setting_section, option='main top',
1031 value=0, type='int',
1032 help='Pixels from screen top to top of main window.'),
1033 Setting(section=self.setting_section, option='main left',
1034 value=0, type='int',
1035 help='Pixels from screen left to left of main window.'),
1036 Setting(section=self.setting_section, option='selected command',
1037 value='load playlist',
1038 help='Name of the initially selected command.'),
1041 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1045 app = HookeApp(gui=self,
1047 inqueue=ui_to_command_queue,
1048 outqueue=command_to_ui_queue,
1052 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1053 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)