1 # Copyright (C) 2008-2010 Fabrizio Benedetti
2 # Massimo Sandal <devicerandom@gmail.com>
3 # Rolf Schmidt <rschmidt@alcor.concordia.ca>
4 # W. Trevor King <wking@drexel.edu>
6 # This file is part of Hooke.
8 # Hooke is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU Lesser General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
13 # Hooke is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
16 # Public License for more details.
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with Hooke. If not, see
20 # <http://www.gnu.org/licenses/>.
22 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
29 wxversion.select(WX_GOOD)
41 import wx.lib.evtmgr as evtmgr
42 # wxPropertyGrid is included in wxPython >= 2.9.1, see
43 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
44 # until then, we'll avoid it because of the *nix build problems.
45 #import wx.propgrid as wxpg
47 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
48 from ...config import Setting
49 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
50 from ...ui import UserInterface, CommandMessage
51 from .dialog.selection import Selection as SelectionDialog
52 from .dialog.save_file import select_save_file
53 from . import menu as menu
54 from . import navbar as navbar
55 from . import panel as panel
56 from .panel.propertyeditor import props_from_argument, props_from_setting
57 from . import statusbar as statusbar
60 class HookeFrame (wx.Frame):
61 """The main Hooke-interface window.
63 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
64 super(HookeFrame, self).__init__(*args, **kwargs)
65 self.log = logging.getLogger('hooke')
67 self.commands = commands
68 self.inqueue = inqueue
69 self.outqueue = outqueue
70 self._perspectives = {} # {name: perspective_str}
73 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
76 self._c['manager'] = aui.AuiManager()
77 self._c['manager'].SetManagedWindow(self)
79 # set the gradient and drag styles
80 self._c['manager'].GetArtProvider().SetMetric(
81 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
82 self._c['manager'].SetFlags(
83 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
85 # Min size for the frame itself isn't completely done. See
86 # the end of FrameManager::Update() for the test code. For
87 # now, just hard code a frame minimum size.
88 #self.SetMinSize(wx.Size(500, 500))
91 self._setup_toolbars()
92 self._c['manager'].Update() # commit pending changes
94 # Create the menubar after the panes so that the default
95 # perspective is created with all panes open
96 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
97 self._c['menu bar'] = menu.HookeMenuBar(
101 'close': self._on_close,
102 'about': self._on_about,
103 'view_panel': self._on_panel_visibility,
104 'save_perspective': self._on_save_perspective,
105 'delete_perspective': self._on_delete_perspective,
106 'select_perspective': self._on_select_perspective,
108 self.SetMenuBar(self._c['menu bar'])
110 self._c['status bar'] = statusbar.StatusBar(
112 style=wx.ST_SIZEGRIP)
113 self.SetStatusBar(self._c['status bar'])
115 self._setup_perspectives()
118 self.execute_command(
119 command=self._command_by_name('load playlist'),
120 args={'input':'test/data/test'},#vclamp_picoforce/playlist'},
122 self.execute_command(
123 command=self._command_by_name('load playlist'),
124 args={'input':'test/data/vclamp_picoforce/playlist'},
126 self.execute_command(
127 command=self._command_by_name('polymer fit'),
128 args={'block':1, 'bounds':[918, 1103]},
130 return # TODO: cleanup
131 self.playlists = self._c['playlist'].Playlists
132 self._displayed_plot = None
133 #load default list, if possible
134 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
139 def _setup_panels(self):
140 client_size = self.GetClientSize()
142 # ('folders', wx.GenericDirCtrl(
144 # dir=self.gui.config['folders-workdir'],
146 # style=wx.DIRCTRL_SHOW_FILTERS,
147 # filter=self.gui.config['folders-filters'],
148 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
149 (panel.PANELS['playlist'](
151 'delete_playlist':self._on_user_delete_playlist,
152 '_delete_playlist':self._on_delete_playlist,
153 'delete_curve':self._on_user_delete_curve,
154 '_delete_curve':self._on_delete_curve,
155 '_on_set_selected_playlist':self._on_set_selected_playlist,
156 '_on_set_selected_curve':self._on_set_selected_curve,
159 style=wx.WANTS_CHARS|wx.NO_BORDER,
160 # WANTS_CHARS so the panel doesn't eat the Return key.
163 (panel.PANELS['note'](
165 '_on_update':self._on_update_note,
168 style=wx.WANTS_CHARS|wx.NO_BORDER,
171 # ('notebook', Notebook(
173 # pos=wx.Point(client_size.x, client_size.y),
174 # size=wx.Size(430, 200),
175 # style=aui.AUI_NB_DEFAULT_STYLE
176 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
177 (panel.PANELS['commands'](
178 commands=self.commands,
179 selected=self.gui.config['selected command'],
181 'execute': self.execute_command,
182 'select_plugin': self.select_plugin,
183 'select_command': self.select_command,
184 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
187 style=wx.WANTS_CHARS|wx.NO_BORDER,
188 # WANTS_CHARS so the panel doesn't eat the Return key.
191 (panel.PANELS['propertyeditor'](
194 style=wx.WANTS_CHARS,
195 # WANTS_CHARS so the panel doesn't eat the Return key.
197 # ('assistant', wx.TextCtrl(
199 # pos=wx.Point(0, 0),
200 # size=wx.Size(150, 90),
201 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
202 (panel.PANELS['plot'](
204 '_set_status_text': self._on_plot_status_text,
207 style=wx.WANTS_CHARS|wx.NO_BORDER,
208 # WANTS_CHARS so the panel doesn't eat the Return key.
211 (panel.PANELS['output'](
214 size=wx.Size(150, 90),
215 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
217 # ('results', panel.results.Results(self), 'bottom'),
219 self._add_panel(p, style)
220 #self._c['assistant'].SetEditable(False)
222 def _add_panel(self, panel, style):
223 self._c[panel.name] = panel
224 m_name = panel.managed_name
225 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
226 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
229 elif style == 'center':
231 elif style == 'left':
233 elif style == 'right':
236 assert style == 'bottom', style
238 self._c['manager'].AddPane(panel, info)
240 def _setup_toolbars(self):
241 self._c['navigation bar'] = navbar.NavBar(
243 'next': self._next_curve,
244 'previous': self._previous_curve,
247 style=wx.TB_FLAT | wx.TB_NODIVIDER)
248 self._c['manager'].AddPane(
249 self._c['navigation bar'],
250 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
251 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
252 ).RightDockable(False))
254 def _bind_events(self):
255 # TODO: figure out if we can use the eventManager for menu
256 # ranges and events of 'self' without raising an assertion
258 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
259 self.Bind(wx.EVT_SIZE, self._on_size)
260 self.Bind(wx.EVT_CLOSE, self._on_close)
261 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
262 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
264 return # TODO: cleanup
265 treeCtrl = self._c['folders'].GetTreeCtrl()
266 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
269 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
271 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
273 def _on_about(self, *args):
274 dialog = wx.MessageDialog(
276 message=self.gui._splash_text(extra_info={
277 'get-details':'click "Help -> License"'},
279 caption='About Hooke',
280 style=wx.OK|wx.ICON_INFORMATION)
284 def _on_close(self, *args):
285 self.log.info('closing GUI framework')
287 self.gui.config['main height'] = str(self.GetSize().GetHeight())
288 self.gui.config['main left'] = str(self.GetPosition()[0])
289 self.gui.config['main top'] = str(self.GetPosition()[1])
290 self.gui.config['main width'] = str(self.GetSize().GetWidth())
291 # push changes back to Hooke.config?
292 self._c['manager'].UnInit()
293 del self._c['manager']
298 # Panel utility functions
300 def _file_name(self, name):
301 """Cleanup names according to configured preferences.
303 if self.gui.config['hide extensions'] == True:
304 name,ext = os.path.splitext(name)
311 def _command_by_name(self, name):
312 cs = [c for c in self.commands if c.name == name]
316 raise Exception('Multiple commands named "%s"' % name)
319 def execute_command(self, _class=None, method=None,
320 command=None, args=None):
323 if ('property editor' in self._c
324 and self.gui.config['selected command'] == command):
325 for name,value in self._c['property editor'].get_values().items():
326 arg = self._c['property editor']._argument_from_label.get(
331 args[arg.name] = value
333 # deal with counted arguments
334 if arg.name not in args:
336 index = int(name[len(arg.name):])
337 args[arg.name][index] = value
338 for arg in command.arguments:
339 if arg.count != 1 and arg.name in args:
340 keys = sorted(args[arg.name].keys())
341 assert keys == range(arg.count), keys
342 args[arg.name] = [args[arg.name][i]
343 for i in range(arg.count)]
344 self.log.debug('executing %s with %s' % (command.name, args))
345 self.inqueue.put(CommandMessage(command, args))
348 msg = self.outqueue.get()
350 if isinstance(msg, Exit):
353 elif isinstance(msg, CommandExit):
354 # TODO: display command complete
356 elif isinstance(msg, ReloadUserInterfaceConfig):
357 self.gui.reload_config(msg.config)
359 elif isinstance(msg, Request):
360 h = handler.HANDLERS[msg.type]
361 h.run(self, msg) # TODO: pause for response?
364 self, '_postprocess_%s' % command.name.replace(' ', '_'),
365 self._postprocess_text)
366 pp(command=command, args=args, results=results)
369 def _handle_request(self, msg):
370 """Repeatedly try to get a response to `msg`.
373 raise NotImplementedError('_%s_request_prompt' % msg.type)
374 prompt_string = prompt(msg)
375 parser = getattr(self, '_%s_request_parser' % msg.type, None)
377 raise NotImplementedError('_%s_request_parser' % msg.type)
381 self.cmd.stdout.write(''.join([
382 error.__class__.__name__, ': ', str(error), '\n']))
383 self.cmd.stdout.write(prompt_string)
384 value = parser(msg, self.cmd.stdin.readline())
386 response = msg.response(value)
388 except ValueError, error:
390 self.inqueue.put(response)
394 # Command-specific postprocessing
396 def _postprocess_text(self, command, args={}, results=[]):
397 """Print the string representation of the results to the Results window.
399 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
400 approach, except that :class:`~hooke.ui.commandline.DoCommand`
401 doesn't print some internally handled messages
402 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
404 for result in results:
405 if isinstance(result, CommandExit):
406 self._c['output'].write(result.__class__.__name__+'\n')
407 self._c['output'].write(str(result).rstrip()+'\n')
409 def _postprocess_load_playlist(self, command, args={}, results=None):
410 """Update `self` to show the playlist.
412 if not isinstance(results[-1], Success):
413 self._postprocess_text(command, results=results)
415 assert len(results) == 2, results
416 playlist = results[0]
417 self._c['playlist']._c['tree'].add_playlist(playlist)
419 def _postprocess_get_playlist(self, command, args={}, results=[]):
420 if not isinstance(results[-1], Success):
421 self._postprocess_text(command, results=results)
423 assert len(results) == 2, results
424 playlist = results[0]
425 self._c['playlist']._c['tree'].update_playlist(playlist)
427 def _postprocess_get_curve(self, command, args={}, results=[]):
428 """Update `self` to show the curve.
430 if not isinstance(results[-1], Success):
431 self._postprocess_text(command, results=results)
433 assert len(results) == 2, results
435 if args.get('curve', None) == None:
436 # the command defaults to the current curve of the current playlist
437 results = self.execute_command(
438 command=self._command_by_name('get playlist'))
439 playlist = results[0]
441 raise NotImplementedError()
442 if 'note' in self._c:
443 self._c['note'].set_text(curve.info['note'])
444 if 'playlist' in self._c:
445 self._c['playlist']._c['tree'].set_selected_curve(
447 if 'plot' in self._c:
448 self._c['plot'].set_curve(curve, config=self.gui.config)
450 def _postprocess_next_curve(self, command, args={}, results=[]):
451 """No-op. Only call 'next curve' via `self._next_curve()`.
455 def _postprocess_previous_curve(self, command, args={}, results=[]):
456 """No-op. Only call 'previous curve' via `self._previous_curve()`.
460 def _postprocess_zero_block_surface_contact_point(
461 self, command, args={}, results=[]):
462 """Update the curve, since the available columns may have changed.
464 if isinstance(results[-1], Success):
465 self.execute_command(
466 command=self._command_by_name('get curve'))
468 def _postprocess_add_block_force_array(
469 self, command, args={}, results=[]):
470 """Update the curve, since the available columns may have changed.
472 if isinstance(results[-1], Success):
473 self.execute_command(
474 command=self._command_by_name('get curve'))
480 def _GetActiveFileIndex(self):
481 lib.playlist.Playlist = self.GetActivePlaylist()
482 #get the selected item from the tree
483 selected_item = self._c['playlist']._c['tree'].GetSelection()
484 #test if a playlist or a curve was double-clicked
485 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
489 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
490 while selected_item.IsOk():
492 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
495 def _GetPlaylistTab(self, name):
496 for index, page in enumerate(self._c['notebook']._tabs._pages):
497 if page.caption == name:
501 def select_plugin(self, _class=None, method=None, plugin=None):
504 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
506 playlist = lib.playlist.Playlist(self, self.drivers)
508 playlist.add_curve(item)
509 if playlist.count > 0:
510 playlist.name = self._GetUniquePlaylistName(name)
512 self.AddTayliss(playlist)
514 def AppliesPlotmanipulator(self, name):
516 Returns True if the plotmanipulator 'name' is applied, False otherwise
517 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
519 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
521 def ApplyPlotmanipulators(self, plot, plot_file):
523 Apply all active plotmanipulators.
525 if plot is not None and plot_file is not None:
526 manipulated_plot = copy.deepcopy(plot)
527 for plotmanipulator in self.plotmanipulators:
528 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
529 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
530 return manipulated_plot
532 def GetActiveFigure(self):
533 playlist_name = self.GetActivePlaylistName()
534 figure = self.playlists[playlist_name].figure
535 if figure is not None:
539 def GetActiveFile(self):
540 playlist = self.GetActivePlaylist()
541 if playlist is not None:
542 return playlist.get_active_file()
545 def GetActivePlot(self):
546 playlist = self.GetActivePlaylist()
547 if playlist is not None:
548 return playlist.get_active_file().plot
551 def GetDisplayedPlot(self):
552 plot = copy.deepcopy(self.displayed_plot)
554 #plot.curves = copy.deepcopy(plot.curves)
557 def GetDisplayedPlotCorrected(self):
558 plot = copy.deepcopy(self.displayed_plot)
560 plot.curves = copy.deepcopy(plot.corrected_curves)
563 def GetDisplayedPlotRaw(self):
564 plot = copy.deepcopy(self.displayed_plot)
566 plot.curves = copy.deepcopy(plot.raw_curves)
569 def GetDockArt(self):
570 return self._c['manager'].GetArtProvider()
572 def GetPlotmanipulator(self, name):
574 Returns a plot manipulator function from its name
576 for plotmanipulator in self.plotmanipulators:
577 if plotmanipulator.name == name:
578 return plotmanipulator
581 def HasPlotmanipulator(self, name):
583 returns True if the plotmanipulator 'name' is loaded, False otherwise
585 for plotmanipulator in self.plotmanipulators:
586 if plotmanipulator.command == name:
591 def _on_dir_ctrl_left_double_click(self, event):
592 file_path = self.panelFolders.GetPath()
593 if os.path.isfile(file_path):
594 if file_path.endswith('.hkp'):
595 self.do_loadlist(file_path)
598 def _on_erase_background(self, event):
601 def _on_notebook_page_close(self, event):
602 ctrl = event.GetEventObject()
603 playlist_name = ctrl.GetPageText(ctrl._curpage)
604 self.DeleteFromPlaylists(playlist_name)
606 def OnPaneClose(self, event):
609 def OnPropGridChanged (self, event):
610 prop = event.GetProperty()
612 item_section = self.panelProperties.SelectedTreeItem
613 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
614 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
615 config = self.gui.config[plugin]
616 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
617 property_key = prop.GetName()
618 property_value = prop.GetDisplayedString()
620 config[property_section][property_key]['value'] = property_value
622 def OnResultsCheck(self, index, flag):
623 results = self.GetActivePlot().results
624 if results.has_key(self.results_str):
625 results[self.results_str].results[index].visible = flag
626 results[self.results_str].update()
630 def _on_size(self, event):
633 def UpdatePlaylistsTreeSelection(self):
634 playlist = self.GetActivePlaylist()
635 if playlist is not None:
636 if playlist.index >= 0:
637 self._c['status bar'].set_playlist(playlist)
641 def _on_curve_select(self, playlist, curve):
642 #create the plot tab and add playlist to the dictionary
643 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
644 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
645 #tab_index = self._c['notebook'].GetSelection()
646 playlist.figure = plotPanel.get_figure()
647 self.playlists[playlist.name] = playlist
648 #self.playlists[playlist.name] = [playlist, figure]
649 self._c['status bar'].set_playlist(playlist)
654 def _on_playlist_left_doubleclick(self):
655 index = self._c['notebook'].GetSelection()
656 current_playlist = self._c['notebook'].GetPageText(index)
657 if current_playlist != playlist_name:
658 index = self._GetPlaylistTab(playlist_name)
659 self._c['notebook'].SetSelection(index)
660 self._c['status bar'].set_playlist(playlist)
664 def _on_playlist_delete(self, playlist):
665 notebook = self.Parent.plotNotebook
666 index = self.Parent._GetPlaylistTab(playlist.name)
667 notebook.SetSelection(index)
668 notebook.DeletePage(notebook.GetSelection())
669 self.Parent.DeleteFromPlaylists(playlist_name)
673 # Command panel interface
675 def select_command(self, _class, method, command):
676 #self.select_plugin(plugin=command.plugin)
677 if 'assistant' in self._c:
678 self._c['assitant'].ChangeValue(command.help)
679 self._c['property editor'].clear()
680 self._c['property editor']._argument_from_label = {}
681 for argument in command.arguments:
682 if argument.name == 'help':
685 results = self.execute_command(
686 command=self._command_by_name('playlists'))
687 if not isinstance(results[-1], Success):
688 self._postprocess_text(command, results=results)
691 playlists = results[0]
693 results = self.execute_command(
694 command=self._command_by_name('playlist curves'))
695 if not isinstance(results[-1], Success):
696 self._postprocess_text(command, results=results)
701 ret = props_from_argument(
702 argument, curves=curves, playlists=playlists)
704 continue # property intentionally not handled (yet)
706 self._c['property editor'].append_property(p)
707 self._c['property editor']._argument_from_label[label] = (
710 self.gui.config['selected command'] = command # TODO: push to engine
714 # Note panel interface
716 def _on_update_note(self, _class, method, text):
717 """Sets the note for the active curve.
719 self.execute_command(
720 command=self._command_by_name('set note'),
725 # Playlist panel interface
727 def _on_user_delete_playlist(self, _class, method, playlist):
730 def _on_delete_playlist(self, _class, method, playlist):
731 if hasattr(playlist, 'path') and playlist.path != None:
732 os.remove(playlist.path)
734 def _on_user_delete_curve(self, _class, method, playlist, curve):
737 def _on_delete_curve(self, _class, method, playlist, curve):
738 # TODO: execute_command 'remove curve from playlist'
739 os.remove(curve.path)
741 def _on_set_selected_playlist(self, _class, method, playlist):
742 """Call the `jump to playlist` command.
744 results = self.execute_command(
745 command=self._command_by_name('playlists'))
746 if not isinstance(results[-1], Success):
748 assert len(results) == 2, results
749 playlists = results[0]
750 matching = [p for p in playlists if p.name == playlist.name]
751 assert len(matching) == 1, matching
752 index = playlists.index(matching[0])
753 results = self.execute_command(
754 command=self._command_by_name('jump to playlist'),
755 args={'index':index})
757 def _on_set_selected_curve(self, _class, method, playlist, curve):
758 """Call the `jump to curve` command.
760 self._on_set_selected_playlist(_class, method, playlist)
761 index = playlist.index(curve)
762 results = self.execute_command(
763 command=self._command_by_name('jump to curve'),
764 args={'index':index})
765 if not isinstance(results[-1], Success):
767 #results = self.execute_command(
768 # command=self._command_by_name('get playlist'))
769 #if not isinstance(results[-1], Success):
771 self.execute_command(
772 command=self._command_by_name('get curve'))
776 # Plot panel interface
778 def _on_plot_status_text(self, _class, method, text):
779 if 'status bar' in self._c:
780 self._c['status bar'].set_plot_text(text)
786 def _next_curve(self, *args):
787 """Call the `next curve` command.
789 results = self.execute_command(
790 command=self._command_by_name('next curve'))
791 if isinstance(results[-1], Success):
792 self.execute_command(
793 command=self._command_by_name('get curve'))
795 def _previous_curve(self, *args):
796 """Call the `previous curve` command.
798 results = self.execute_command(
799 command=self._command_by_name('previous curve'))
800 if isinstance(results[-1], Success):
801 self.execute_command(
802 command=self._command_by_name('get curve'))
806 # Panel display handling
808 def _on_panel_visibility(self, _class, method, panel_name, visible):
809 pane = self._c['manager'].GetPane(panel_name)
811 #if we don't do the following, the Folders pane does not resize properly on hide/show
812 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
813 #folders_size = pane.GetSize()
814 self.panelFolders.Fit()
815 self._c['manager'].Update()
817 def _setup_perspectives(self):
818 """Add perspectives to menubar and _perspectives.
820 self._perspectives = {
821 'Default': self._c['manager'].SavePerspective(),
823 path = self.gui.config['perspective path']
824 if os.path.isdir(path):
825 files = sorted(os.listdir(path))
827 name, extension = os.path.splitext(fname)
828 if extension != self.gui.config['perspective extension']:
830 fpath = os.path.join(path, fname)
831 if not os.path.isfile(fpath):
834 with open(fpath, 'rU') as f:
835 perspective = f.readline()
837 self._perspectives[name] = perspective
839 selected_perspective = self.gui.config['active perspective']
840 if not self._perspectives.has_key(selected_perspective):
841 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
843 self._restore_perspective(selected_perspective, force=True)
844 self._update_perspective_menu()
846 def _update_perspective_menu(self):
847 self._c['menu bar']._c['perspective'].update(
848 sorted(self._perspectives.keys()),
849 self.gui.config['active perspective'])
851 def _save_perspective(self, perspective, perspective_dir, name,
853 path = os.path.join(perspective_dir, name)
854 if extension != None:
856 if not os.path.isdir(perspective_dir):
857 os.makedirs(perspective_dir)
858 with open(path, 'w') as f:
860 self._perspectives[name] = perspective
861 self._restore_perspective(name)
862 self._update_perspective_menu()
864 def _delete_perspectives(self, perspective_dir, names,
866 self.log.debug('remove perspectives %s from %s'
867 % (names, perspective_dir))
869 path = os.path.join(perspective_dir, name)
870 if extension != None:
873 del(self._perspectives[name])
874 self._update_perspective_menu()
875 if self.gui.config['active perspective'] in names:
876 self._restore_perspective('Default')
877 # TODO: does this bug still apply?
878 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
879 # http://trac.wxwidgets.org/ticket/3258
880 # ) that makes the radio item indicator in the menu disappear.
881 # The code should be fine once this issue is fixed.
883 def _restore_perspective(self, name, force=False):
884 if name != self.gui.config['active perspective'] or force == True:
885 self.log.debug('restore perspective %s' % name)
886 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
887 self._c['manager'].LoadPerspective(self._perspectives[name])
888 self._c['manager'].Update()
889 for pane in self._c['manager'].GetAllPanes():
890 view = self._c['menu bar']._c['view']
891 if pane.name in view._c.keys():
892 view._c[pane.name].Check(pane.window.IsShown())
894 def _on_save_perspective(self, *args):
895 perspective = self._c['manager'].SavePerspective()
896 name = self.gui.config['active perspective']
897 if name == 'Default':
898 name = 'New perspective'
899 name = select_save_file(
900 directory=self.gui.config['perspective path'],
902 extension=self.gui.config['perspective extension'],
904 message='Enter a name for the new perspective:',
905 caption='Save perspective')
908 self._save_perspective(
909 perspective, self.gui.config['perspective path'], name=name,
910 extension=self.gui.config['perspective extension'])
912 def _on_delete_perspective(self, *args, **kwargs):
913 options = sorted([p for p in self._perspectives.keys()
915 dialog = SelectionDialog(
917 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
918 button_id=wx.ID_DELETE,
919 selection_style='multiple',
921 title='Delete perspective(s)',
922 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
923 dialog.CenterOnScreen()
925 if dialog.canceled == True:
927 names = [options[i] for i in dialog.selected]
929 self._delete_perspectives(
930 self.gui.config['perspective path'], names=names,
931 extension=self.gui.config['perspective extension'])
933 def _on_select_perspective(self, _class, method, name):
934 self._restore_perspective(name)
938 class HookeApp (wx.App):
939 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
941 Tosses up a splash screen and then loads :class:`HookeFrame` in
944 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
946 self.commands = commands
947 self.inqueue = inqueue
948 self.outqueue = outqueue
949 super(HookeApp, self).__init__(*args, **kwargs)
952 self.SetAppName('Hooke')
953 self.SetVendorName('')
954 self._setup_splash_screen()
956 height = self.gui.config['main height']
957 width = self.gui.config['main width']
958 top = self.gui.config['main top']
959 left = self.gui.config['main left']
961 # Sometimes, the ini file gets confused and sets 'left' and
962 # 'top' to large negative numbers. Here we catch and fix
963 # this. Keep small negative numbers, the user might want
972 self.gui, self.commands, self.inqueue, self.outqueue,
973 parent=None, title='Hooke',
974 pos=(left, top), size=(width, height),
975 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
977 self._c['frame'].Show(True)
978 self.SetTopWindow(self._c['frame'])
981 def _setup_splash_screen(self):
982 if self.gui.config['show splash screen'] == True:
983 path = self.gui.config['splash screen image']
984 if os.path.isfile(path):
985 duration = self.gui.config['splash screen duration']
987 bitmap=wx.Image(path).ConvertToBitmap(),
988 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
989 milliseconds=duration,
992 # For some reason splashDuration and sleep do not
993 # correspond to each other at least not on Windows.
994 # Maybe it's because duration is in milliseconds and
995 # sleep in seconds. Thus we need to increase the
996 # sleep time a bit. A factor of 1.2 seems to work.
998 time.sleep(sleepFactor * duration / 1000)
1001 class GUI (UserInterface):
1002 """wxWindows graphical user interface.
1005 super(GUI, self).__init__(name='gui')
1007 def default_settings(self):
1008 """Return a list of :class:`hooke.config.Setting`\s for any
1009 configurable UI settings.
1011 The suggested section setting is::
1013 Setting(section=self.setting_section, help=self.__doc__)
1016 Setting(section=self.setting_section, help=self.__doc__),
1017 Setting(section=self.setting_section, option='icon image',
1018 value=os.path.join('doc', 'img', 'microscope.ico'),
1020 help='Path to the hooke icon image.'),
1021 Setting(section=self.setting_section, option='show splash screen',
1022 value=True, type='bool',
1023 help='Enable/disable the splash screen'),
1024 Setting(section=self.setting_section, option='splash screen image',
1025 value=os.path.join('doc', 'img', 'hooke.jpg'),
1027 help='Path to the Hooke splash screen image.'),
1028 Setting(section=self.setting_section,
1029 option='splash screen duration',
1030 value=1000, type='int',
1031 help='Duration of the splash screen in milliseconds.'),
1032 Setting(section=self.setting_section, option='perspective path',
1033 value=os.path.join('resources', 'gui', 'perspective'),
1034 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1035 Setting(section=self.setting_section, option='perspective extension',
1037 help='Extension for perspective files.'),
1038 Setting(section=self.setting_section, option='hide extensions',
1039 value=False, type='bool',
1040 help='Hide file extensions when displaying names.'),
1041 Setting(section=self.setting_section, option='plot legend',
1042 value=True, type='bool',
1043 help='Enable/disable the plot legend.'),
1044 Setting(section=self.setting_section, option='plot SI format',
1045 value='True', type='bool',
1046 help='Enable/disable SI plot axes numbering.'),
1047 Setting(section=self.setting_section, option='plot decimals',
1048 value=2, type='int',
1049 help='Number of decimal places to show if "plot SI format" is enabled.'),
1050 Setting(section=self.setting_section, option='folders-workdir',
1051 value='.', type='path',
1052 help='This should probably go...'),
1053 Setting(section=self.setting_section, option='folders-filters',
1054 value='.', type='path',
1055 help='This should probably go...'),
1056 Setting(section=self.setting_section, option='active perspective',
1058 help='Name of active perspective file (or "Default").'),
1059 Setting(section=self.setting_section,
1060 option='folders-filter-index',
1061 value=0, type='int',
1062 help='This should probably go...'),
1063 Setting(section=self.setting_section, option='main height',
1064 value=450, type='int',
1065 help='Height of main window in pixels.'),
1066 Setting(section=self.setting_section, option='main width',
1067 value=800, type='int',
1068 help='Width of main window in pixels.'),
1069 Setting(section=self.setting_section, option='main top',
1070 value=0, type='int',
1071 help='Pixels from screen top to top of main window.'),
1072 Setting(section=self.setting_section, option='main left',
1073 value=0, type='int',
1074 help='Pixels from screen left to left of main window.'),
1075 Setting(section=self.setting_section, option='selected command',
1076 value='load playlist',
1077 help='Name of the initially selected command.'),
1080 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1084 app = HookeApp(gui=self,
1086 inqueue=ui_to_command_queue,
1087 outqueue=command_to_ui_queue,
1091 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1092 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)