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 ...engine import CommandMessage
50 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
51 from ...ui import UserInterface
52 from .dialog.selection import Selection as SelectionDialog
53 from .dialog.save_file import select_save_file
54 from . import menu as menu
55 from . import navbar as navbar
56 from . import panel as panel
57 from .panel.propertyeditor import props_from_argument, props_from_setting
58 from . import statusbar as statusbar
61 class HookeFrame (wx.Frame):
62 """The main Hooke-interface window.
64 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
65 super(HookeFrame, self).__init__(*args, **kwargs)
66 self.log = logging.getLogger('hooke')
68 self.commands = commands
69 self.inqueue = inqueue
70 self.outqueue = outqueue
71 self._perspectives = {} # {name: perspective_str}
74 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
77 self._c['manager'] = aui.AuiManager()
78 self._c['manager'].SetManagedWindow(self)
80 # set the gradient and drag styles
81 self._c['manager'].GetArtProvider().SetMetric(
82 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
83 self._c['manager'].SetFlags(
84 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
86 # Min size for the frame itself isn't completely done. See
87 # the end of FrameManager::Update() for the test code. For
88 # now, just hard code a frame minimum size.
89 #self.SetMinSize(wx.Size(500, 500))
92 self._setup_toolbars()
93 self._c['manager'].Update() # commit pending changes
95 # Create the menubar after the panes so that the default
96 # perspective is created with all panes open
97 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
98 self._c['menu bar'] = menu.HookeMenuBar(
102 'close': self._on_close,
103 'about': self._on_about,
104 'view_panel': self._on_panel_visibility,
105 'save_perspective': self._on_save_perspective,
106 'delete_perspective': self._on_delete_perspective,
107 'select_perspective': self._on_select_perspective,
109 self.SetMenuBar(self._c['menu bar'])
111 self._c['status bar'] = statusbar.StatusBar(
113 style=wx.ST_SIZEGRIP)
114 self.SetStatusBar(self._c['status bar'])
116 self._setup_perspectives()
118 return # TODO: cleanup
119 self._displayed_plot = None
120 #load default list, if possible
121 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
126 def _setup_panels(self):
127 client_size = self.GetClientSize()
129 # ('folders', wx.GenericDirCtrl(
131 # dir=self.gui.config['folders-workdir'],
133 # style=wx.DIRCTRL_SHOW_FILTERS,
134 # filter=self.gui.config['folders-filters'],
135 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
136 (panel.PANELS['playlist'](
138 'delete_playlist':self._on_user_delete_playlist,
139 '_delete_playlist':self._on_delete_playlist,
140 'delete_curve':self._on_user_delete_curve,
141 '_delete_curve':self._on_delete_curve,
142 '_on_set_selected_playlist':self._on_set_selected_playlist,
143 '_on_set_selected_curve':self._on_set_selected_curve,
146 style=wx.WANTS_CHARS|wx.NO_BORDER,
147 # WANTS_CHARS so the panel doesn't eat the Return key.
150 (panel.PANELS['note'](
152 '_on_update':self._on_update_note,
155 style=wx.WANTS_CHARS|wx.NO_BORDER,
158 # ('notebook', Notebook(
160 # pos=wx.Point(client_size.x, client_size.y),
161 # size=wx.Size(430, 200),
162 # style=aui.AUI_NB_DEFAULT_STYLE
163 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
164 (panel.PANELS['commands'](
165 commands=self.commands,
166 selected=self.gui.config['selected command'],
168 'execute': self.execute_command,
169 'select_plugin': self.select_plugin,
170 'select_command': self.select_command,
171 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
174 style=wx.WANTS_CHARS|wx.NO_BORDER,
175 # WANTS_CHARS so the panel doesn't eat the Return key.
178 (panel.PANELS['propertyeditor'](
181 style=wx.WANTS_CHARS,
182 # WANTS_CHARS so the panel doesn't eat the Return key.
184 (panel.PANELS['plot'](
186 '_set_status_text': self._on_plot_status_text,
189 style=wx.WANTS_CHARS|wx.NO_BORDER,
190 # WANTS_CHARS so the panel doesn't eat the Return key.
193 (panel.PANELS['output'](
196 size=wx.Size(150, 90),
197 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
199 # ('results', panel.results.Results(self), 'bottom'),
201 self._add_panel(p, style)
202 self.execute_command( # setup already loaded playlists
203 command=self._command_by_name('playlists'))
204 self.execute_command( # setup already loaded curve
205 command=self._command_by_name('get curve'))
207 def _add_panel(self, panel, style):
208 self._c[panel.name] = panel
209 m_name = panel.managed_name
210 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
211 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
214 elif style == 'center':
216 elif style == 'left':
218 elif style == 'right':
221 assert style == 'bottom', style
223 self._c['manager'].AddPane(panel, info)
225 def _setup_toolbars(self):
226 self._c['navigation bar'] = navbar.NavBar(
228 'next': self._next_curve,
229 'previous': self._previous_curve,
232 style=wx.TB_FLAT | wx.TB_NODIVIDER)
233 self._c['manager'].AddPane(
234 self._c['navigation bar'],
235 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
236 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
237 ).RightDockable(False))
239 def _bind_events(self):
240 # TODO: figure out if we can use the eventManager for menu
241 # ranges and events of 'self' without raising an assertion
243 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
244 self.Bind(wx.EVT_SIZE, self._on_size)
245 self.Bind(wx.EVT_CLOSE, self._on_close)
246 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
247 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
249 return # TODO: cleanup
250 treeCtrl = self._c['folders'].GetTreeCtrl()
251 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
254 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
256 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
258 def _on_about(self, *args):
259 dialog = wx.MessageDialog(
261 message=self.gui._splash_text(extra_info={
262 'get-details':'click "Help -> License"'},
264 caption='About Hooke',
265 style=wx.OK|wx.ICON_INFORMATION)
269 def _on_close(self, *args):
270 self.log.info('closing GUI framework')
272 self.gui.config['main height'] = str(self.GetSize().GetHeight())
273 self.gui.config['main left'] = str(self.GetPosition()[0])
274 self.gui.config['main top'] = str(self.GetPosition()[1])
275 self.gui.config['main width'] = str(self.GetSize().GetWidth())
276 # push changes back to Hooke.config?
277 self._c['manager'].UnInit()
278 del self._c['manager']
283 # Panel utility functions
285 def _file_name(self, name):
286 """Cleanup names according to configured preferences.
288 if self.gui.config['hide extensions'] == True:
289 name,ext = os.path.splitext(name)
296 def _command_by_name(self, name):
297 cs = [c for c in self.commands if c.name == name]
301 raise Exception('Multiple commands named "%s"' % name)
304 def execute_command(self, _class=None, method=None,
305 command=None, args=None):
308 if ('property editor' in self._c
309 and self.gui.config['selected command'] == command):
310 for name,value in self._c['property editor'].get_values().items():
311 arg = self._c['property editor']._argument_from_label.get(
316 args[arg.name] = value
318 # deal with counted arguments
319 if arg.name not in args:
321 index = int(name[len(arg.name):])
322 args[arg.name][index] = value
323 for arg in command.arguments:
325 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
326 count = arg._display_count
327 if count != 1 and arg.name in args:
328 keys = sorted(args[arg.name].keys())
329 assert keys == range(count), keys
330 args[arg.name] = [args[arg.name][i]
331 for i in range(count)]
333 while (len(args[arg.name]) > 0
334 and args[arg.name][-1] == None):
336 if len(args[arg.name]) == 0:
337 args[arg.name] = arg.default
338 cm = CommandMessage(self.command.name, args)
339 self.log.debug('executing %s' % cm)
343 msg = self.outqueue.get()
345 if isinstance(msg, Exit):
348 elif isinstance(msg, CommandExit):
349 # TODO: display command complete
351 elif isinstance(msg, ReloadUserInterfaceConfig):
352 self.gui.reload_config(msg.config)
354 elif isinstance(msg, Request):
355 h = handler.HANDLERS[msg.type]
356 h.run(self, msg) # TODO: pause for response?
359 self, '_postprocess_%s' % command.name.replace(' ', '_'),
360 self._postprocess_text)
361 pp(command=command, args=args, results=results)
364 def _handle_request(self, msg):
365 """Repeatedly try to get a response to `msg`.
368 raise NotImplementedError('_%s_request_prompt' % msg.type)
369 prompt_string = prompt(msg)
370 parser = getattr(self, '_%s_request_parser' % msg.type, None)
372 raise NotImplementedError('_%s_request_parser' % msg.type)
376 self.cmd.stdout.write(''.join([
377 error.__class__.__name__, ': ', str(error), '\n']))
378 self.cmd.stdout.write(prompt_string)
379 value = parser(msg, self.cmd.stdin.readline())
381 response = msg.response(value)
383 except ValueError, error:
385 self.inqueue.put(response)
389 # Command-specific postprocessing
391 def _postprocess_text(self, command, args={}, results=[]):
392 """Print the string representation of the results to the Results window.
394 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
395 approach, except that :class:`~hooke.ui.commandline.DoCommand`
396 doesn't print some internally handled messages
397 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
399 for result in results:
400 if isinstance(result, CommandExit):
401 self._c['output'].write(result.__class__.__name__+'\n')
402 self._c['output'].write(str(result).rstrip()+'\n')
404 def _postprocess_playlists(self, command, args={}, results=None):
405 """Update `self` to show the playlists.
407 if not isinstance(results[-1], Success):
408 self._postprocess_text(command, results=results)
410 assert len(results) == 2, results
411 playlists = results[0]
412 loaded_playlists = [] # TODO
413 if 'playlist' in self._c:
414 for playlist in playlists:
415 if playlist in loaded_playlists:
416 self._c['playlist'].update_playlist(playlist)
418 self._c['playlist'].add_playlist(playlist)
420 def _postprocess_load_playlist(self, command, args={}, results=None):
421 """Update `self` to show the playlist.
423 if not isinstance(results[-1], Success):
424 self._postprocess_text(command, results=results)
426 assert len(results) == 2, results
427 playlist = results[0]
428 self._c['playlist'].add_playlist(playlist)
430 def _postprocess_get_playlist(self, command, args={}, results=[]):
431 if not isinstance(results[-1], Success):
432 self._postprocess_text(command, results=results)
434 assert len(results) == 2, results
435 playlist = results[0]
436 self._c['playlist'].update_playlist(playlist)
438 def _postprocess_get_curve(self, command, args={}, results=[]):
439 """Update `self` to show the curve.
441 if not isinstance(results[-1], Success):
442 self._postprocess_text(command, results=results)
444 assert len(results) == 2, results
446 if args.get('curve', None) == None:
447 # the command defaults to the current curve of the current playlist
448 results = self.execute_command(
449 command=self._command_by_name('get playlist'))
450 playlist = results[0]
452 raise NotImplementedError()
453 if 'note' in self._c:
454 self._c['note'].set_text(curve.info['note'])
455 if 'playlist' in self._c:
456 self._c['playlist'].set_selected_curve(
458 if 'plot' in self._c:
459 self._c['plot'].set_curve(curve, config=self.gui.config)
461 def _postprocess_next_curve(self, command, args={}, results=[]):
462 """No-op. Only call 'next curve' via `self._next_curve()`.
466 def _postprocess_previous_curve(self, command, args={}, results=[]):
467 """No-op. Only call 'previous curve' via `self._previous_curve()`.
471 def _postprocess_zero_block_surface_contact_point(
472 self, command, args={}, results=[]):
473 """Update the curve, since the available columns may have changed.
475 if isinstance(results[-1], Success):
476 self.execute_command(
477 command=self._command_by_name('get curve'))
479 def _postprocess_add_block_force_array(
480 self, command, args={}, results=[]):
481 """Update the curve, since the available columns may have changed.
483 if isinstance(results[-1], Success):
484 self.execute_command(
485 command=self._command_by_name('get curve'))
491 def _GetActiveFileIndex(self):
492 lib.playlist.Playlist = self.GetActivePlaylist()
493 #get the selected item from the tree
494 selected_item = self._c['playlist']._c['tree'].GetSelection()
495 #test if a playlist or a curve was double-clicked
496 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
500 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
501 while selected_item.IsOk():
503 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
506 def _GetPlaylistTab(self, name):
507 for index, page in enumerate(self._c['notebook']._tabs._pages):
508 if page.caption == name:
512 def select_plugin(self, _class=None, method=None, plugin=None):
515 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
517 playlist = lib.playlist.Playlist(self, self.drivers)
519 playlist.add_curve(item)
520 if playlist.count > 0:
521 playlist.name = self._GetUniquePlaylistName(name)
523 self.AddTayliss(playlist)
525 def AppliesPlotmanipulator(self, name):
527 Returns True if the plotmanipulator 'name' is applied, False otherwise
528 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
530 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
532 def ApplyPlotmanipulators(self, plot, plot_file):
534 Apply all active plotmanipulators.
536 if plot is not None and plot_file is not None:
537 manipulated_plot = copy.deepcopy(plot)
538 for plotmanipulator in self.plotmanipulators:
539 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
540 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
541 return manipulated_plot
543 def GetActiveFigure(self):
544 playlist_name = self.GetActivePlaylistName()
545 figure = self.playlists[playlist_name].figure
546 if figure is not None:
550 def GetActiveFile(self):
551 playlist = self.GetActivePlaylist()
552 if playlist is not None:
553 return playlist.get_active_file()
556 def GetActivePlot(self):
557 playlist = self.GetActivePlaylist()
558 if playlist is not None:
559 return playlist.get_active_file().plot
562 def GetDisplayedPlot(self):
563 plot = copy.deepcopy(self.displayed_plot)
565 #plot.curves = copy.deepcopy(plot.curves)
568 def GetDisplayedPlotCorrected(self):
569 plot = copy.deepcopy(self.displayed_plot)
571 plot.curves = copy.deepcopy(plot.corrected_curves)
574 def GetDisplayedPlotRaw(self):
575 plot = copy.deepcopy(self.displayed_plot)
577 plot.curves = copy.deepcopy(plot.raw_curves)
580 def GetDockArt(self):
581 return self._c['manager'].GetArtProvider()
583 def GetPlotmanipulator(self, name):
585 Returns a plot manipulator function from its name
587 for plotmanipulator in self.plotmanipulators:
588 if plotmanipulator.name == name:
589 return plotmanipulator
592 def HasPlotmanipulator(self, name):
594 returns True if the plotmanipulator 'name' is loaded, False otherwise
596 for plotmanipulator in self.plotmanipulators:
597 if plotmanipulator.command == name:
602 def _on_dir_ctrl_left_double_click(self, event):
603 file_path = self.panelFolders.GetPath()
604 if os.path.isfile(file_path):
605 if file_path.endswith('.hkp'):
606 self.do_loadlist(file_path)
609 def _on_erase_background(self, event):
612 def _on_notebook_page_close(self, event):
613 ctrl = event.GetEventObject()
614 playlist_name = ctrl.GetPageText(ctrl._curpage)
615 self.DeleteFromPlaylists(playlist_name)
617 def OnPaneClose(self, event):
620 def OnPropGridChanged (self, event):
621 prop = event.GetProperty()
623 item_section = self.panelProperties.SelectedTreeItem
624 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
625 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
626 config = self.gui.config[plugin]
627 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
628 property_key = prop.GetName()
629 property_value = prop.GetDisplayedString()
631 config[property_section][property_key]['value'] = property_value
633 def OnResultsCheck(self, index, flag):
634 results = self.GetActivePlot().results
635 if results.has_key(self.results_str):
636 results[self.results_str].results[index].visible = flag
637 results[self.results_str].update()
641 def _on_size(self, event):
644 def UpdatePlaylistsTreeSelection(self):
645 playlist = self.GetActivePlaylist()
646 if playlist is not None:
647 if playlist.index >= 0:
648 self._c['status bar'].set_playlist(playlist)
652 def _on_curve_select(self, playlist, curve):
653 #create the plot tab and add playlist to the dictionary
654 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
655 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
656 #tab_index = self._c['notebook'].GetSelection()
657 playlist.figure = plotPanel.get_figure()
658 self.playlists[playlist.name] = playlist
659 #self.playlists[playlist.name] = [playlist, figure]
660 self._c['status bar'].set_playlist(playlist)
665 def _on_playlist_left_doubleclick(self):
666 index = self._c['notebook'].GetSelection()
667 current_playlist = self._c['notebook'].GetPageText(index)
668 if current_playlist != playlist_name:
669 index = self._GetPlaylistTab(playlist_name)
670 self._c['notebook'].SetSelection(index)
671 self._c['status bar'].set_playlist(playlist)
675 def _on_playlist_delete(self, playlist):
676 notebook = self.Parent.plotNotebook
677 index = self.Parent._GetPlaylistTab(playlist.name)
678 notebook.SetSelection(index)
679 notebook.DeletePage(notebook.GetSelection())
680 self.Parent.DeleteFromPlaylists(playlist_name)
684 # Command panel interface
686 def select_command(self, _class, method, command):
687 #self.select_plugin(plugin=command.plugin)
688 self._c['property editor'].clear()
689 self._c['property editor']._argument_from_label = {}
690 for argument in command.arguments:
691 if argument.name == 'help':
694 results = self.execute_command(
695 command=self._command_by_name('playlists'))
696 if not isinstance(results[-1], Success):
697 self._postprocess_text(command, results=results)
700 playlists = results[0]
702 results = self.execute_command(
703 command=self._command_by_name('playlist curves'))
704 if not isinstance(results[-1], Success):
705 self._postprocess_text(command, results=results)
710 ret = props_from_argument(
711 argument, curves=curves, playlists=playlists)
713 continue # property intentionally not handled (yet)
715 self._c['property editor'].append_property(p)
716 self._c['property editor']._argument_from_label[label] = (
719 self.gui.config['selected command'] = command # TODO: push to engine
723 # Note panel interface
725 def _on_update_note(self, _class, method, text):
726 """Sets the note for the active curve.
728 self.execute_command(
729 command=self._command_by_name('set note'),
734 # Playlist panel interface
736 def _on_user_delete_playlist(self, _class, method, playlist):
739 def _on_delete_playlist(self, _class, method, playlist):
740 if hasattr(playlist, 'path') and playlist.path != None:
741 os.remove(playlist.path)
743 def _on_user_delete_curve(self, _class, method, playlist, curve):
746 def _on_delete_curve(self, _class, method, playlist, curve):
747 # TODO: execute_command 'remove curve from playlist'
748 os.remove(curve.path)
750 def _on_set_selected_playlist(self, _class, method, playlist):
751 """Call the `jump to playlist` command.
753 results = self.execute_command(
754 command=self._command_by_name('playlists'))
755 if not isinstance(results[-1], Success):
757 assert len(results) == 2, results
758 playlists = results[0]
759 matching = [p for p in playlists if p.name == playlist.name]
760 assert len(matching) == 1, matching
761 index = playlists.index(matching[0])
762 results = self.execute_command(
763 command=self._command_by_name('jump to playlist'),
764 args={'index':index})
766 def _on_set_selected_curve(self, _class, method, playlist, curve):
767 """Call the `jump to curve` command.
769 self._on_set_selected_playlist(_class, method, playlist)
770 index = playlist.index(curve)
771 results = self.execute_command(
772 command=self._command_by_name('jump to curve'),
773 args={'index':index})
774 if not isinstance(results[-1], Success):
776 #results = self.execute_command(
777 # command=self._command_by_name('get playlist'))
778 #if not isinstance(results[-1], Success):
780 self.execute_command(
781 command=self._command_by_name('get curve'))
785 # Plot panel interface
787 def _on_plot_status_text(self, _class, method, text):
788 if 'status bar' in self._c:
789 self._c['status bar'].set_plot_text(text)
795 def _next_curve(self, *args):
796 """Call the `next curve` command.
798 results = self.execute_command(
799 command=self._command_by_name('next curve'))
800 if isinstance(results[-1], Success):
801 self.execute_command(
802 command=self._command_by_name('get curve'))
804 def _previous_curve(self, *args):
805 """Call the `previous curve` command.
807 results = self.execute_command(
808 command=self._command_by_name('previous curve'))
809 if isinstance(results[-1], Success):
810 self.execute_command(
811 command=self._command_by_name('get curve'))
815 # Panel display handling
817 def _on_panel_visibility(self, _class, method, panel_name, visible):
818 pane = self._c['manager'].GetPane(panel_name)
820 #if we don't do the following, the Folders pane does not resize properly on hide/show
821 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
822 #folders_size = pane.GetSize()
823 self.panelFolders.Fit()
824 self._c['manager'].Update()
826 def _setup_perspectives(self):
827 """Add perspectives to menubar and _perspectives.
829 self._perspectives = {
830 'Default': self._c['manager'].SavePerspective(),
832 path = self.gui.config['perspective path']
833 if os.path.isdir(path):
834 files = sorted(os.listdir(path))
836 name, extension = os.path.splitext(fname)
837 if extension != self.gui.config['perspective extension']:
839 fpath = os.path.join(path, fname)
840 if not os.path.isfile(fpath):
843 with open(fpath, 'rU') as f:
844 perspective = f.readline()
846 self._perspectives[name] = perspective
848 selected_perspective = self.gui.config['active perspective']
849 if not self._perspectives.has_key(selected_perspective):
850 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
852 self._restore_perspective(selected_perspective, force=True)
853 self._update_perspective_menu()
855 def _update_perspective_menu(self):
856 self._c['menu bar']._c['perspective'].update(
857 sorted(self._perspectives.keys()),
858 self.gui.config['active perspective'])
860 def _save_perspective(self, perspective, perspective_dir, name,
862 path = os.path.join(perspective_dir, name)
863 if extension != None:
865 if not os.path.isdir(perspective_dir):
866 os.makedirs(perspective_dir)
867 with open(path, 'w') as f:
869 self._perspectives[name] = perspective
870 self._restore_perspective(name)
871 self._update_perspective_menu()
873 def _delete_perspectives(self, perspective_dir, names,
875 self.log.debug('remove perspectives %s from %s'
876 % (names, perspective_dir))
878 path = os.path.join(perspective_dir, name)
879 if extension != None:
882 del(self._perspectives[name])
883 self._update_perspective_menu()
884 if self.gui.config['active perspective'] in names:
885 self._restore_perspective('Default')
886 # TODO: does this bug still apply?
887 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
888 # http://trac.wxwidgets.org/ticket/3258
889 # ) that makes the radio item indicator in the menu disappear.
890 # The code should be fine once this issue is fixed.
892 def _restore_perspective(self, name, force=False):
893 if name != self.gui.config['active perspective'] or force == True:
894 self.log.debug('restore perspective %s' % name)
895 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
896 self._c['manager'].LoadPerspective(self._perspectives[name])
897 self._c['manager'].Update()
898 for pane in self._c['manager'].GetAllPanes():
899 view = self._c['menu bar']._c['view']
900 if pane.name in view._c.keys():
901 view._c[pane.name].Check(pane.window.IsShown())
903 def _on_save_perspective(self, *args):
904 perspective = self._c['manager'].SavePerspective()
905 name = self.gui.config['active perspective']
906 if name == 'Default':
907 name = 'New perspective'
908 name = select_save_file(
909 directory=self.gui.config['perspective path'],
911 extension=self.gui.config['perspective extension'],
913 message='Enter a name for the new perspective:',
914 caption='Save perspective')
917 self._save_perspective(
918 perspective, self.gui.config['perspective path'], name=name,
919 extension=self.gui.config['perspective extension'])
921 def _on_delete_perspective(self, *args, **kwargs):
922 options = sorted([p for p in self._perspectives.keys()
924 dialog = SelectionDialog(
926 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
927 button_id=wx.ID_DELETE,
928 selection_style='multiple',
930 title='Delete perspective(s)',
931 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
932 dialog.CenterOnScreen()
934 if dialog.canceled == True:
936 names = [options[i] for i in dialog.selected]
938 self._delete_perspectives(
939 self.gui.config['perspective path'], names=names,
940 extension=self.gui.config['perspective extension'])
942 def _on_select_perspective(self, _class, method, name):
943 self._restore_perspective(name)
947 class HookeApp (wx.App):
948 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
950 Tosses up a splash screen and then loads :class:`HookeFrame` in
953 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
955 self.commands = commands
956 self.inqueue = inqueue
957 self.outqueue = outqueue
958 super(HookeApp, self).__init__(*args, **kwargs)
961 self.SetAppName('Hooke')
962 self.SetVendorName('')
963 self._setup_splash_screen()
965 height = self.gui.config['main height']
966 width = self.gui.config['main width']
967 top = self.gui.config['main top']
968 left = self.gui.config['main left']
970 # Sometimes, the ini file gets confused and sets 'left' and
971 # 'top' to large negative numbers. Here we catch and fix
972 # this. Keep small negative numbers, the user might want
981 self.gui, self.commands, self.inqueue, self.outqueue,
982 parent=None, title='Hooke',
983 pos=(left, top), size=(width, height),
984 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
986 self._c['frame'].Show(True)
987 self.SetTopWindow(self._c['frame'])
990 def _setup_splash_screen(self):
991 if self.gui.config['show splash screen'] == True:
992 path = self.gui.config['splash screen image']
993 if os.path.isfile(path):
994 duration = self.gui.config['splash screen duration']
996 bitmap=wx.Image(path).ConvertToBitmap(),
997 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
998 milliseconds=duration,
1001 # For some reason splashDuration and sleep do not
1002 # correspond to each other at least not on Windows.
1003 # Maybe it's because duration is in milliseconds and
1004 # sleep in seconds. Thus we need to increase the
1005 # sleep time a bit. A factor of 1.2 seems to work.
1007 time.sleep(sleepFactor * duration / 1000)
1010 class GUI (UserInterface):
1011 """wxWindows graphical user interface.
1014 super(GUI, self).__init__(name='gui')
1016 def default_settings(self):
1017 """Return a list of :class:`hooke.config.Setting`\s for any
1018 configurable UI settings.
1020 The suggested section setting is::
1022 Setting(section=self.setting_section, help=self.__doc__)
1025 Setting(section=self.setting_section, help=self.__doc__),
1026 Setting(section=self.setting_section, option='icon image',
1027 value=os.path.join('doc', 'img', 'microscope.ico'),
1029 help='Path to the hooke icon image.'),
1030 Setting(section=self.setting_section, option='show splash screen',
1031 value=True, type='bool',
1032 help='Enable/disable the splash screen'),
1033 Setting(section=self.setting_section, option='splash screen image',
1034 value=os.path.join('doc', 'img', 'hooke.jpg'),
1036 help='Path to the Hooke splash screen image.'),
1037 Setting(section=self.setting_section,
1038 option='splash screen duration',
1039 value=1000, type='int',
1040 help='Duration of the splash screen in milliseconds.'),
1041 Setting(section=self.setting_section, option='perspective path',
1042 value=os.path.join('resources', 'gui', 'perspective'),
1043 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1044 Setting(section=self.setting_section, option='perspective extension',
1046 help='Extension for perspective files.'),
1047 Setting(section=self.setting_section, option='hide extensions',
1048 value=False, type='bool',
1049 help='Hide file extensions when displaying names.'),
1050 Setting(section=self.setting_section, option='plot legend',
1051 value=True, type='bool',
1052 help='Enable/disable the plot legend.'),
1053 Setting(section=self.setting_section, option='plot SI format',
1054 value='True', type='bool',
1055 help='Enable/disable SI plot axes numbering.'),
1056 Setting(section=self.setting_section, option='plot decimals',
1057 value=2, type='int',
1058 help='Number of decimal places to show if "plot SI format" is enabled.'),
1059 Setting(section=self.setting_section, option='folders-workdir',
1060 value='.', type='path',
1061 help='This should probably go...'),
1062 Setting(section=self.setting_section, option='folders-filters',
1063 value='.', type='path',
1064 help='This should probably go...'),
1065 Setting(section=self.setting_section, option='active perspective',
1067 help='Name of active perspective file (or "Default").'),
1068 Setting(section=self.setting_section,
1069 option='folders-filter-index',
1070 value=0, type='int',
1071 help='This should probably go...'),
1072 Setting(section=self.setting_section, option='main height',
1073 value=450, type='int',
1074 help='Height of main window in pixels.'),
1075 Setting(section=self.setting_section, option='main width',
1076 value=800, type='int',
1077 help='Width of main window in pixels.'),
1078 Setting(section=self.setting_section, option='main top',
1079 value=0, type='int',
1080 help='Pixels from screen top to top of main window.'),
1081 Setting(section=self.setting_section, option='main left',
1082 value=0, type='int',
1083 help='Pixels from screen left to left of main window.'),
1084 Setting(section=self.setting_section, option='selected command',
1085 value='load playlist',
1086 help='Name of the initially selected command.'),
1089 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1093 app = HookeApp(gui=self,
1095 inqueue=ui_to_command_queue,
1096 outqueue=command_to_ui_queue,
1100 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1101 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)