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:
324 if arg.name not in args:
325 continue # undisplayed argument, e.g. 'driver' types.
327 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
328 count = arg._display_count
329 if count != 1 and arg.name in args:
330 keys = sorted(args[arg.name].keys())
331 assert keys == range(count), keys
332 args[arg.name] = [args[arg.name][i]
333 for i in range(count)]
335 while (len(args[arg.name]) > 0
336 and args[arg.name][-1] == None):
338 if len(args[arg.name]) == 0:
339 args[arg.name] = arg.default
340 cm = CommandMessage(command.name, args)
341 self.gui._submit_command(cm, self.inqueue)
344 msg = self.outqueue.get()
346 if isinstance(msg, Exit):
349 elif isinstance(msg, CommandExit):
350 # TODO: display command complete
352 elif isinstance(msg, ReloadUserInterfaceConfig):
353 self.gui.reload_config(msg.config)
355 elif isinstance(msg, Request):
356 h = handler.HANDLERS[msg.type]
357 h.run(self, msg) # TODO: pause for response?
360 self, '_postprocess_%s' % command.name.replace(' ', '_'),
361 self._postprocess_text)
362 pp(command=command, args=args, results=results)
365 def _handle_request(self, msg):
366 """Repeatedly try to get a response to `msg`.
369 raise NotImplementedError('_%s_request_prompt' % msg.type)
370 prompt_string = prompt(msg)
371 parser = getattr(self, '_%s_request_parser' % msg.type, None)
373 raise NotImplementedError('_%s_request_parser' % msg.type)
377 self.cmd.stdout.write(''.join([
378 error.__class__.__name__, ': ', str(error), '\n']))
379 self.cmd.stdout.write(prompt_string)
380 value = parser(msg, self.cmd.stdin.readline())
382 response = msg.response(value)
384 except ValueError, error:
386 self.inqueue.put(response)
390 # Command-specific postprocessing
392 def _postprocess_text(self, command, args={}, results=[]):
393 """Print the string representation of the results to the Results window.
395 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
396 approach, except that :class:`~hooke.ui.commandline.DoCommand`
397 doesn't print some internally handled messages
398 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
400 for result in results:
401 if isinstance(result, CommandExit):
402 self._c['output'].write(result.__class__.__name__+'\n')
403 self._c['output'].write(str(result).rstrip()+'\n')
405 def _postprocess_playlists(self, command, args={}, results=None):
406 """Update `self` to show the playlists.
408 if not isinstance(results[-1], Success):
409 self._postprocess_text(command, results=results)
411 assert len(results) == 2, results
412 playlists = results[0]
413 loaded_playlists = [] # TODO
414 if 'playlist' in self._c:
415 for playlist in playlists:
416 if playlist in loaded_playlists:
417 self._c['playlist'].update_playlist(playlist)
419 self._c['playlist'].add_playlist(playlist)
421 def _postprocess_load_playlist(self, command, args={}, results=None):
422 """Update `self` to show the playlist.
424 if not isinstance(results[-1], Success):
425 self._postprocess_text(command, results=results)
427 assert len(results) == 2, results
428 playlist = results[0]
429 self._c['playlist'].add_playlist(playlist)
431 def _postprocess_get_playlist(self, command, args={}, results=[]):
432 if not isinstance(results[-1], Success):
433 self._postprocess_text(command, results=results)
435 assert len(results) == 2, results
436 playlist = results[0]
437 self._c['playlist'].update_playlist(playlist)
439 def _postprocess_get_curve(self, command, args={}, results=[]):
440 """Update `self` to show the curve.
442 if not isinstance(results[-1], Success):
443 self._postprocess_text(command, results=results)
445 assert len(results) == 2, results
447 if args.get('curve', None) == None:
448 # the command defaults to the current curve of the current playlist
449 results = self.execute_command(
450 command=self._command_by_name('get playlist'))
451 playlist = results[0]
453 raise NotImplementedError()
454 if 'note' in self._c:
455 self._c['note'].set_text(curve.info['note'])
456 if 'playlist' in self._c:
457 self._c['playlist'].set_selected_curve(
459 if 'plot' in self._c:
460 self._c['plot'].set_curve(curve, config=self.gui.config)
462 def _postprocess_next_curve(self, command, args={}, results=[]):
463 """No-op. Only call 'next curve' via `self._next_curve()`.
467 def _postprocess_previous_curve(self, command, args={}, results=[]):
468 """No-op. Only call 'previous curve' via `self._previous_curve()`.
472 def _postprocess_zero_block_surface_contact_point(
473 self, command, args={}, results=[]):
474 """Update the curve, since the available columns may have changed.
476 if isinstance(results[-1], Success):
477 self.execute_command(
478 command=self._command_by_name('get curve'))
480 def _postprocess_add_block_force_array(
481 self, command, args={}, results=[]):
482 """Update the curve, since the available columns may have changed.
484 if isinstance(results[-1], Success):
485 self.execute_command(
486 command=self._command_by_name('get curve'))
492 def _GetActiveFileIndex(self):
493 lib.playlist.Playlist = self.GetActivePlaylist()
494 #get the selected item from the tree
495 selected_item = self._c['playlist']._c['tree'].GetSelection()
496 #test if a playlist or a curve was double-clicked
497 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
501 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
502 while selected_item.IsOk():
504 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
507 def _GetPlaylistTab(self, name):
508 for index, page in enumerate(self._c['notebook']._tabs._pages):
509 if page.caption == name:
513 def select_plugin(self, _class=None, method=None, plugin=None):
516 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
518 playlist = lib.playlist.Playlist(self, self.drivers)
520 playlist.add_curve(item)
521 if playlist.count > 0:
522 playlist.name = self._GetUniquePlaylistName(name)
524 self.AddTayliss(playlist)
526 def AppliesPlotmanipulator(self, name):
528 Returns True if the plotmanipulator 'name' is applied, False otherwise
529 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
531 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
533 def ApplyPlotmanipulators(self, plot, plot_file):
535 Apply all active plotmanipulators.
537 if plot is not None and plot_file is not None:
538 manipulated_plot = copy.deepcopy(plot)
539 for plotmanipulator in self.plotmanipulators:
540 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
541 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
542 return manipulated_plot
544 def GetActiveFigure(self):
545 playlist_name = self.GetActivePlaylistName()
546 figure = self.playlists[playlist_name].figure
547 if figure is not None:
551 def GetActiveFile(self):
552 playlist = self.GetActivePlaylist()
553 if playlist is not None:
554 return playlist.get_active_file()
557 def GetActivePlot(self):
558 playlist = self.GetActivePlaylist()
559 if playlist is not None:
560 return playlist.get_active_file().plot
563 def GetDisplayedPlot(self):
564 plot = copy.deepcopy(self.displayed_plot)
566 #plot.curves = copy.deepcopy(plot.curves)
569 def GetDisplayedPlotCorrected(self):
570 plot = copy.deepcopy(self.displayed_plot)
572 plot.curves = copy.deepcopy(plot.corrected_curves)
575 def GetDisplayedPlotRaw(self):
576 plot = copy.deepcopy(self.displayed_plot)
578 plot.curves = copy.deepcopy(plot.raw_curves)
581 def GetDockArt(self):
582 return self._c['manager'].GetArtProvider()
584 def GetPlotmanipulator(self, name):
586 Returns a plot manipulator function from its name
588 for plotmanipulator in self.plotmanipulators:
589 if plotmanipulator.name == name:
590 return plotmanipulator
593 def HasPlotmanipulator(self, name):
595 returns True if the plotmanipulator 'name' is loaded, False otherwise
597 for plotmanipulator in self.plotmanipulators:
598 if plotmanipulator.command == name:
603 def _on_dir_ctrl_left_double_click(self, event):
604 file_path = self.panelFolders.GetPath()
605 if os.path.isfile(file_path):
606 if file_path.endswith('.hkp'):
607 self.do_loadlist(file_path)
610 def _on_erase_background(self, event):
613 def _on_notebook_page_close(self, event):
614 ctrl = event.GetEventObject()
615 playlist_name = ctrl.GetPageText(ctrl._curpage)
616 self.DeleteFromPlaylists(playlist_name)
618 def OnPaneClose(self, event):
621 def OnPropGridChanged (self, event):
622 prop = event.GetProperty()
624 item_section = self.panelProperties.SelectedTreeItem
625 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
626 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
627 config = self.gui.config[plugin]
628 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
629 property_key = prop.GetName()
630 property_value = prop.GetDisplayedString()
632 config[property_section][property_key]['value'] = property_value
634 def OnResultsCheck(self, index, flag):
635 results = self.GetActivePlot().results
636 if results.has_key(self.results_str):
637 results[self.results_str].results[index].visible = flag
638 results[self.results_str].update()
642 def _on_size(self, event):
645 def UpdatePlaylistsTreeSelection(self):
646 playlist = self.GetActivePlaylist()
647 if playlist is not None:
648 if playlist.index >= 0:
649 self._c['status bar'].set_playlist(playlist)
653 def _on_curve_select(self, playlist, curve):
654 #create the plot tab and add playlist to the dictionary
655 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
656 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
657 #tab_index = self._c['notebook'].GetSelection()
658 playlist.figure = plotPanel.get_figure()
659 self.playlists[playlist.name] = playlist
660 #self.playlists[playlist.name] = [playlist, figure]
661 self._c['status bar'].set_playlist(playlist)
666 def _on_playlist_left_doubleclick(self):
667 index = self._c['notebook'].GetSelection()
668 current_playlist = self._c['notebook'].GetPageText(index)
669 if current_playlist != playlist_name:
670 index = self._GetPlaylistTab(playlist_name)
671 self._c['notebook'].SetSelection(index)
672 self._c['status bar'].set_playlist(playlist)
676 def _on_playlist_delete(self, playlist):
677 notebook = self.Parent.plotNotebook
678 index = self.Parent._GetPlaylistTab(playlist.name)
679 notebook.SetSelection(index)
680 notebook.DeletePage(notebook.GetSelection())
681 self.Parent.DeleteFromPlaylists(playlist_name)
685 # Command panel interface
687 def select_command(self, _class, method, command):
688 #self.select_plugin(plugin=command.plugin)
689 self._c['property editor'].clear()
690 self._c['property editor']._argument_from_label = {}
691 for argument in command.arguments:
692 if argument.name == 'help':
695 results = self.execute_command(
696 command=self._command_by_name('playlists'))
697 if not isinstance(results[-1], Success):
698 self._postprocess_text(command, results=results)
701 playlists = results[0]
703 results = self.execute_command(
704 command=self._command_by_name('playlist curves'))
705 if not isinstance(results[-1], Success):
706 self._postprocess_text(command, results=results)
711 ret = props_from_argument(
712 argument, curves=curves, playlists=playlists)
714 continue # property intentionally not handled (yet)
716 self._c['property editor'].append_property(p)
717 self._c['property editor']._argument_from_label[label] = (
720 self.gui.config['selected command'] = command # TODO: push to engine
724 # Note panel interface
726 def _on_update_note(self, _class, method, text):
727 """Sets the note for the active curve.
729 self.execute_command(
730 command=self._command_by_name('set note'),
735 # Playlist panel interface
737 def _on_user_delete_playlist(self, _class, method, playlist):
740 def _on_delete_playlist(self, _class, method, playlist):
741 if hasattr(playlist, 'path') and playlist.path != None:
742 os.remove(playlist.path)
744 def _on_user_delete_curve(self, _class, method, playlist, curve):
747 def _on_delete_curve(self, _class, method, playlist, curve):
748 # TODO: execute_command 'remove curve from playlist'
749 os.remove(curve.path)
751 def _on_set_selected_playlist(self, _class, method, playlist):
752 """Call the `jump to playlist` command.
754 results = self.execute_command(
755 command=self._command_by_name('playlists'))
756 if not isinstance(results[-1], Success):
758 assert len(results) == 2, results
759 playlists = results[0]
760 matching = [p for p in playlists if p.name == playlist.name]
761 assert len(matching) == 1, matching
762 index = playlists.index(matching[0])
763 results = self.execute_command(
764 command=self._command_by_name('jump to playlist'),
765 args={'index':index})
767 def _on_set_selected_curve(self, _class, method, playlist, curve):
768 """Call the `jump to curve` command.
770 self._on_set_selected_playlist(_class, method, playlist)
771 index = playlist.index(curve)
772 results = self.execute_command(
773 command=self._command_by_name('jump to curve'),
774 args={'index':index})
775 if not isinstance(results[-1], Success):
777 #results = self.execute_command(
778 # command=self._command_by_name('get playlist'))
779 #if not isinstance(results[-1], Success):
781 self.execute_command(
782 command=self._command_by_name('get curve'))
786 # Plot panel interface
788 def _on_plot_status_text(self, _class, method, text):
789 if 'status bar' in self._c:
790 self._c['status bar'].set_plot_text(text)
796 def _next_curve(self, *args):
797 """Call the `next curve` command.
799 results = self.execute_command(
800 command=self._command_by_name('next curve'))
801 if isinstance(results[-1], Success):
802 self.execute_command(
803 command=self._command_by_name('get curve'))
805 def _previous_curve(self, *args):
806 """Call the `previous curve` command.
808 results = self.execute_command(
809 command=self._command_by_name('previous curve'))
810 if isinstance(results[-1], Success):
811 self.execute_command(
812 command=self._command_by_name('get curve'))
816 # Panel display handling
818 def _on_panel_visibility(self, _class, method, panel_name, visible):
819 pane = self._c['manager'].GetPane(panel_name)
821 #if we don't do the following, the Folders pane does not resize properly on hide/show
822 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
823 #folders_size = pane.GetSize()
824 self.panelFolders.Fit()
825 self._c['manager'].Update()
827 def _setup_perspectives(self):
828 """Add perspectives to menubar and _perspectives.
830 self._perspectives = {
831 'Default': self._c['manager'].SavePerspective(),
833 path = self.gui.config['perspective path']
834 if os.path.isdir(path):
835 files = sorted(os.listdir(path))
837 name, extension = os.path.splitext(fname)
838 if extension != self.gui.config['perspective extension']:
840 fpath = os.path.join(path, fname)
841 if not os.path.isfile(fpath):
844 with open(fpath, 'rU') as f:
845 perspective = f.readline()
847 self._perspectives[name] = perspective
849 selected_perspective = self.gui.config['active perspective']
850 if not self._perspectives.has_key(selected_perspective):
851 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
853 self._restore_perspective(selected_perspective, force=True)
854 self._update_perspective_menu()
856 def _update_perspective_menu(self):
857 self._c['menu bar']._c['perspective'].update(
858 sorted(self._perspectives.keys()),
859 self.gui.config['active perspective'])
861 def _save_perspective(self, perspective, perspective_dir, name,
863 path = os.path.join(perspective_dir, name)
864 if extension != None:
866 if not os.path.isdir(perspective_dir):
867 os.makedirs(perspective_dir)
868 with open(path, 'w') as f:
870 self._perspectives[name] = perspective
871 self._restore_perspective(name)
872 self._update_perspective_menu()
874 def _delete_perspectives(self, perspective_dir, names,
876 self.log.debug('remove perspectives %s from %s'
877 % (names, perspective_dir))
879 path = os.path.join(perspective_dir, name)
880 if extension != None:
883 del(self._perspectives[name])
884 self._update_perspective_menu()
885 if self.gui.config['active perspective'] in names:
886 self._restore_perspective('Default')
887 # TODO: does this bug still apply?
888 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
889 # http://trac.wxwidgets.org/ticket/3258
890 # ) that makes the radio item indicator in the menu disappear.
891 # The code should be fine once this issue is fixed.
893 def _restore_perspective(self, name, force=False):
894 if name != self.gui.config['active perspective'] or force == True:
895 self.log.debug('restore perspective %s' % name)
896 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
897 self._c['manager'].LoadPerspective(self._perspectives[name])
898 self._c['manager'].Update()
899 for pane in self._c['manager'].GetAllPanes():
900 view = self._c['menu bar']._c['view']
901 if pane.name in view._c.keys():
902 view._c[pane.name].Check(pane.window.IsShown())
904 def _on_save_perspective(self, *args):
905 perspective = self._c['manager'].SavePerspective()
906 name = self.gui.config['active perspective']
907 if name == 'Default':
908 name = 'New perspective'
909 name = select_save_file(
910 directory=self.gui.config['perspective path'],
912 extension=self.gui.config['perspective extension'],
914 message='Enter a name for the new perspective:',
915 caption='Save perspective')
918 self._save_perspective(
919 perspective, self.gui.config['perspective path'], name=name,
920 extension=self.gui.config['perspective extension'])
922 def _on_delete_perspective(self, *args, **kwargs):
923 options = sorted([p for p in self._perspectives.keys()
925 dialog = SelectionDialog(
927 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
928 button_id=wx.ID_DELETE,
929 selection_style='multiple',
931 title='Delete perspective(s)',
932 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
933 dialog.CenterOnScreen()
935 if dialog.canceled == True:
937 names = [options[i] for i in dialog.selected]
939 self._delete_perspectives(
940 self.gui.config['perspective path'], names=names,
941 extension=self.gui.config['perspective extension'])
943 def _on_select_perspective(self, _class, method, name):
944 self._restore_perspective(name)
948 class HookeApp (wx.App):
949 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
951 Tosses up a splash screen and then loads :class:`HookeFrame` in
954 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
956 self.commands = commands
957 self.inqueue = inqueue
958 self.outqueue = outqueue
959 super(HookeApp, self).__init__(*args, **kwargs)
962 self.SetAppName('Hooke')
963 self.SetVendorName('')
964 self._setup_splash_screen()
966 height = self.gui.config['main height']
967 width = self.gui.config['main width']
968 top = self.gui.config['main top']
969 left = self.gui.config['main left']
971 # Sometimes, the ini file gets confused and sets 'left' and
972 # 'top' to large negative numbers. Here we catch and fix
973 # this. Keep small negative numbers, the user might want
982 self.gui, self.commands, self.inqueue, self.outqueue,
983 parent=None, title='Hooke',
984 pos=(left, top), size=(width, height),
985 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
987 self._c['frame'].Show(True)
988 self.SetTopWindow(self._c['frame'])
991 def _setup_splash_screen(self):
992 if self.gui.config['show splash screen'] == True:
993 path = self.gui.config['splash screen image']
994 if os.path.isfile(path):
995 duration = self.gui.config['splash screen duration']
997 bitmap=wx.Image(path).ConvertToBitmap(),
998 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
999 milliseconds=duration,
1002 # For some reason splashDuration and sleep do not
1003 # correspond to each other at least not on Windows.
1004 # Maybe it's because duration is in milliseconds and
1005 # sleep in seconds. Thus we need to increase the
1006 # sleep time a bit. A factor of 1.2 seems to work.
1008 time.sleep(sleepFactor * duration / 1000)
1011 class GUI (UserInterface):
1012 """wxWindows graphical user interface.
1015 super(GUI, self).__init__(name='gui')
1017 def default_settings(self):
1018 """Return a list of :class:`hooke.config.Setting`\s for any
1019 configurable UI settings.
1021 The suggested section setting is::
1023 Setting(section=self.setting_section, help=self.__doc__)
1026 Setting(section=self.setting_section, help=self.__doc__),
1027 Setting(section=self.setting_section, option='icon image',
1028 value=os.path.join('doc', 'img', 'microscope.ico'),
1030 help='Path to the hooke icon image.'),
1031 Setting(section=self.setting_section, option='show splash screen',
1032 value=True, type='bool',
1033 help='Enable/disable the splash screen'),
1034 Setting(section=self.setting_section, option='splash screen image',
1035 value=os.path.join('doc', 'img', 'hooke.jpg'),
1037 help='Path to the Hooke splash screen image.'),
1038 Setting(section=self.setting_section,
1039 option='splash screen duration',
1040 value=1000, type='int',
1041 help='Duration of the splash screen in milliseconds.'),
1042 Setting(section=self.setting_section, option='perspective path',
1043 value=os.path.join('resources', 'gui', 'perspective'),
1044 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1045 Setting(section=self.setting_section, option='perspective extension',
1047 help='Extension for perspective files.'),
1048 Setting(section=self.setting_section, option='hide extensions',
1049 value=False, type='bool',
1050 help='Hide file extensions when displaying names.'),
1051 Setting(section=self.setting_section, option='plot legend',
1052 value=True, type='bool',
1053 help='Enable/disable the plot legend.'),
1054 Setting(section=self.setting_section, option='plot SI format',
1055 value='True', type='bool',
1056 help='Enable/disable SI plot axes numbering.'),
1057 Setting(section=self.setting_section, option='plot decimals',
1058 value=2, type='int',
1059 help='Number of decimal places to show if "plot SI format" is enabled.'),
1060 Setting(section=self.setting_section, option='folders-workdir',
1061 value='.', type='path',
1062 help='This should probably go...'),
1063 Setting(section=self.setting_section, option='folders-filters',
1064 value='.', type='path',
1065 help='This should probably go...'),
1066 Setting(section=self.setting_section, option='active perspective',
1068 help='Name of active perspective file (or "Default").'),
1069 Setting(section=self.setting_section,
1070 option='folders-filter-index',
1071 value=0, type='int',
1072 help='This should probably go...'),
1073 Setting(section=self.setting_section, option='main height',
1074 value=450, type='int',
1075 help='Height of main window in pixels.'),
1076 Setting(section=self.setting_section, option='main width',
1077 value=800, type='int',
1078 help='Width of main window in pixels.'),
1079 Setting(section=self.setting_section, option='main top',
1080 value=0, type='int',
1081 help='Pixels from screen top to top of main window.'),
1082 Setting(section=self.setting_section, option='main left',
1083 value=0, type='int',
1084 help='Pixels from screen left to left of main window.'),
1085 Setting(section=self.setting_section, option='selected command',
1086 value='load playlist',
1087 help='Name of the initially selected command.'),
1090 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1094 app = HookeApp(gui=self,
1096 inqueue=ui_to_command_queue,
1097 outqueue=command_to_ui_queue,
1101 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1102 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)