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()
117 return # TODO: cleanup
118 self._displayed_plot = None
119 #load default list, if possible
120 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
125 def _setup_panels(self):
126 client_size = self.GetClientSize()
128 # ('folders', wx.GenericDirCtrl(
130 # dir=self.gui.config['folders-workdir'],
132 # style=wx.DIRCTRL_SHOW_FILTERS,
133 # filter=self.gui.config['folders-filters'],
134 # defaultFilter=self.gui.config['folders-filter-index']), 'left'),
135 (panel.PANELS['playlist'](
137 'delete_playlist':self._on_user_delete_playlist,
138 '_delete_playlist':self._on_delete_playlist,
139 'delete_curve':self._on_user_delete_curve,
140 '_delete_curve':self._on_delete_curve,
141 '_on_set_selected_playlist':self._on_set_selected_playlist,
142 '_on_set_selected_curve':self._on_set_selected_curve,
145 style=wx.WANTS_CHARS|wx.NO_BORDER,
146 # WANTS_CHARS so the panel doesn't eat the Return key.
149 (panel.PANELS['note'](
151 '_on_update':self._on_update_note,
154 style=wx.WANTS_CHARS|wx.NO_BORDER,
157 # ('notebook', Notebook(
159 # pos=wx.Point(client_size.x, client_size.y),
160 # size=wx.Size(430, 200),
161 # style=aui.AUI_NB_DEFAULT_STYLE
162 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
163 (panel.PANELS['commands'](
164 commands=self.commands,
165 selected=self.gui.config['selected command'],
167 'execute': self.execute_command,
168 'select_plugin': self.select_plugin,
169 'select_command': self.select_command,
170 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
173 style=wx.WANTS_CHARS|wx.NO_BORDER,
174 # WANTS_CHARS so the panel doesn't eat the Return key.
177 (panel.PANELS['propertyeditor'](
180 style=wx.WANTS_CHARS,
181 # WANTS_CHARS so the panel doesn't eat the Return key.
183 (panel.PANELS['plot'](
185 '_set_status_text': self._on_plot_status_text,
188 style=wx.WANTS_CHARS|wx.NO_BORDER,
189 # WANTS_CHARS so the panel doesn't eat the Return key.
192 (panel.PANELS['output'](
195 size=wx.Size(150, 90),
196 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
198 # ('results', panel.results.Results(self), 'bottom'),
200 self._add_panel(p, style)
201 self.execute_command( # setup already loaded playlists
202 command=self._command_by_name('playlists'))
203 self.execute_command( # setup already loaded curve
204 command=self._command_by_name('get curve'))
206 def _add_panel(self, panel, style):
207 self._c[panel.name] = panel
208 m_name = panel.managed_name
209 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
210 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
213 elif style == 'center':
215 elif style == 'left':
217 elif style == 'right':
220 assert style == 'bottom', style
222 self._c['manager'].AddPane(panel, info)
224 def _setup_toolbars(self):
225 self._c['navigation bar'] = navbar.NavBar(
227 'next': self._next_curve,
228 'previous': self._previous_curve,
231 style=wx.TB_FLAT | wx.TB_NODIVIDER)
232 self._c['manager'].AddPane(
233 self._c['navigation bar'],
234 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
235 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
236 ).RightDockable(False))
238 def _bind_events(self):
239 # TODO: figure out if we can use the eventManager for menu
240 # ranges and events of 'self' without raising an assertion
242 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
243 self.Bind(wx.EVT_SIZE, self._on_size)
244 self.Bind(wx.EVT_CLOSE, self._on_close)
245 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
246 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
248 return # TODO: cleanup
249 treeCtrl = self._c['folders'].GetTreeCtrl()
250 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
253 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
255 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
257 def _on_about(self, *args):
258 dialog = wx.MessageDialog(
260 message=self.gui._splash_text(extra_info={
261 'get-details':'click "Help -> License"'},
263 caption='About Hooke',
264 style=wx.OK|wx.ICON_INFORMATION)
268 def _on_close(self, *args):
269 self.log.info('closing GUI framework')
271 self.gui.config['main height'] = str(self.GetSize().GetHeight())
272 self.gui.config['main left'] = str(self.GetPosition()[0])
273 self.gui.config['main top'] = str(self.GetPosition()[1])
274 self.gui.config['main width'] = str(self.GetSize().GetWidth())
275 # push changes back to Hooke.config?
276 self._c['manager'].UnInit()
277 del self._c['manager']
282 # Panel utility functions
284 def _file_name(self, name):
285 """Cleanup names according to configured preferences.
287 if self.gui.config['hide extensions'] == True:
288 name,ext = os.path.splitext(name)
295 def _command_by_name(self, name):
296 cs = [c for c in self.commands if c.name == name]
300 raise Exception('Multiple commands named "%s"' % name)
303 def execute_command(self, _class=None, method=None,
304 command=None, args=None):
307 if ('property editor' in self._c
308 and self.gui.config['selected command'] == command):
309 for name,value in self._c['property editor'].get_values().items():
310 arg = self._c['property editor']._argument_from_label.get(
315 args[arg.name] = value
317 # deal with counted arguments
318 if arg.name not in args:
320 index = int(name[len(arg.name):])
321 args[arg.name][index] = value
322 for arg in command.arguments:
324 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
325 count = arg._display_count
326 if count != 1 and arg.name in args:
327 keys = sorted(args[arg.name].keys())
328 assert keys == range(count), keys
329 args[arg.name] = [args[arg.name][i]
330 for i in range(count)]
332 while (len(args[arg.name]) > 0
333 and args[arg.name][-1] == None):
335 if len(args[arg.name]) == 0:
336 args[arg.name] = arg.default
337 self.log.debug('executing %s with %s' % (command.name, args))
338 self.inqueue.put(CommandMessage(command, args))
341 msg = self.outqueue.get()
343 if isinstance(msg, Exit):
346 elif isinstance(msg, CommandExit):
347 # TODO: display command complete
349 elif isinstance(msg, ReloadUserInterfaceConfig):
350 self.gui.reload_config(msg.config)
352 elif isinstance(msg, Request):
353 h = handler.HANDLERS[msg.type]
354 h.run(self, msg) # TODO: pause for response?
357 self, '_postprocess_%s' % command.name.replace(' ', '_'),
358 self._postprocess_text)
359 pp(command=command, args=args, results=results)
362 def _handle_request(self, msg):
363 """Repeatedly try to get a response to `msg`.
366 raise NotImplementedError('_%s_request_prompt' % msg.type)
367 prompt_string = prompt(msg)
368 parser = getattr(self, '_%s_request_parser' % msg.type, None)
370 raise NotImplementedError('_%s_request_parser' % msg.type)
374 self.cmd.stdout.write(''.join([
375 error.__class__.__name__, ': ', str(error), '\n']))
376 self.cmd.stdout.write(prompt_string)
377 value = parser(msg, self.cmd.stdin.readline())
379 response = msg.response(value)
381 except ValueError, error:
383 self.inqueue.put(response)
387 # Command-specific postprocessing
389 def _postprocess_text(self, command, args={}, results=[]):
390 """Print the string representation of the results to the Results window.
392 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
393 approach, except that :class:`~hooke.ui.commandline.DoCommand`
394 doesn't print some internally handled messages
395 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
397 for result in results:
398 if isinstance(result, CommandExit):
399 self._c['output'].write(result.__class__.__name__+'\n')
400 self._c['output'].write(str(result).rstrip()+'\n')
402 def _postprocess_playlists(self, command, args={}, results=None):
403 """Update `self` to show the playlists.
405 if not isinstance(results[-1], Success):
406 self._postprocess_text(command, results=results)
408 assert len(results) == 2, results
409 playlists = results[0]
410 loaded_playlists = [] # TODO
411 if 'playlist' in self._c:
412 for playlist in playlists:
413 if playlist in loaded_playlists:
414 self._c['playlist'].update_playlist(playlist)
416 self._c['playlist'].add_playlist(playlist)
418 def _postprocess_load_playlist(self, command, args={}, results=None):
419 """Update `self` to show the playlist.
421 if not isinstance(results[-1], Success):
422 self._postprocess_text(command, results=results)
424 assert len(results) == 2, results
425 playlist = results[0]
426 self._c['playlist'].add_playlist(playlist)
428 def _postprocess_get_playlist(self, command, args={}, results=[]):
429 if not isinstance(results[-1], Success):
430 self._postprocess_text(command, results=results)
432 assert len(results) == 2, results
433 playlist = results[0]
434 self._c['playlist'].update_playlist(playlist)
436 def _postprocess_get_curve(self, command, args={}, results=[]):
437 """Update `self` to show the curve.
439 if not isinstance(results[-1], Success):
440 self._postprocess_text(command, results=results)
442 assert len(results) == 2, results
444 if args.get('curve', None) == None:
445 # the command defaults to the current curve of the current playlist
446 results = self.execute_command(
447 command=self._command_by_name('get playlist'))
448 playlist = results[0]
450 raise NotImplementedError()
451 if 'note' in self._c:
452 self._c['note'].set_text(curve.info['note'])
453 if 'playlist' in self._c:
454 self._c['playlist'].set_selected_curve(
456 if 'plot' in self._c:
457 self._c['plot'].set_curve(curve, config=self.gui.config)
459 def _postprocess_next_curve(self, command, args={}, results=[]):
460 """No-op. Only call 'next curve' via `self._next_curve()`.
464 def _postprocess_previous_curve(self, command, args={}, results=[]):
465 """No-op. Only call 'previous curve' via `self._previous_curve()`.
469 def _postprocess_zero_block_surface_contact_point(
470 self, command, args={}, results=[]):
471 """Update the curve, since the available columns may have changed.
473 if isinstance(results[-1], Success):
474 self.execute_command(
475 command=self._command_by_name('get curve'))
477 def _postprocess_add_block_force_array(
478 self, command, args={}, results=[]):
479 """Update the curve, since the available columns may have changed.
481 if isinstance(results[-1], Success):
482 self.execute_command(
483 command=self._command_by_name('get curve'))
489 def _GetActiveFileIndex(self):
490 lib.playlist.Playlist = self.GetActivePlaylist()
491 #get the selected item from the tree
492 selected_item = self._c['playlist']._c['tree'].GetSelection()
493 #test if a playlist or a curve was double-clicked
494 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
498 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
499 while selected_item.IsOk():
501 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
504 def _GetPlaylistTab(self, name):
505 for index, page in enumerate(self._c['notebook']._tabs._pages):
506 if page.caption == name:
510 def select_plugin(self, _class=None, method=None, plugin=None):
513 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
515 playlist = lib.playlist.Playlist(self, self.drivers)
517 playlist.add_curve(item)
518 if playlist.count > 0:
519 playlist.name = self._GetUniquePlaylistName(name)
521 self.AddTayliss(playlist)
523 def AppliesPlotmanipulator(self, name):
525 Returns True if the plotmanipulator 'name' is applied, False otherwise
526 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
528 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
530 def ApplyPlotmanipulators(self, plot, plot_file):
532 Apply all active plotmanipulators.
534 if plot is not None and plot_file is not None:
535 manipulated_plot = copy.deepcopy(plot)
536 for plotmanipulator in self.plotmanipulators:
537 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
538 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
539 return manipulated_plot
541 def GetActiveFigure(self):
542 playlist_name = self.GetActivePlaylistName()
543 figure = self.playlists[playlist_name].figure
544 if figure is not None:
548 def GetActiveFile(self):
549 playlist = self.GetActivePlaylist()
550 if playlist is not None:
551 return playlist.get_active_file()
554 def GetActivePlot(self):
555 playlist = self.GetActivePlaylist()
556 if playlist is not None:
557 return playlist.get_active_file().plot
560 def GetDisplayedPlot(self):
561 plot = copy.deepcopy(self.displayed_plot)
563 #plot.curves = copy.deepcopy(plot.curves)
566 def GetDisplayedPlotCorrected(self):
567 plot = copy.deepcopy(self.displayed_plot)
569 plot.curves = copy.deepcopy(plot.corrected_curves)
572 def GetDisplayedPlotRaw(self):
573 plot = copy.deepcopy(self.displayed_plot)
575 plot.curves = copy.deepcopy(plot.raw_curves)
578 def GetDockArt(self):
579 return self._c['manager'].GetArtProvider()
581 def GetPlotmanipulator(self, name):
583 Returns a plot manipulator function from its name
585 for plotmanipulator in self.plotmanipulators:
586 if plotmanipulator.name == name:
587 return plotmanipulator
590 def HasPlotmanipulator(self, name):
592 returns True if the plotmanipulator 'name' is loaded, False otherwise
594 for plotmanipulator in self.plotmanipulators:
595 if plotmanipulator.command == name:
600 def _on_dir_ctrl_left_double_click(self, event):
601 file_path = self.panelFolders.GetPath()
602 if os.path.isfile(file_path):
603 if file_path.endswith('.hkp'):
604 self.do_loadlist(file_path)
607 def _on_erase_background(self, event):
610 def _on_notebook_page_close(self, event):
611 ctrl = event.GetEventObject()
612 playlist_name = ctrl.GetPageText(ctrl._curpage)
613 self.DeleteFromPlaylists(playlist_name)
615 def OnPaneClose(self, event):
618 def OnPropGridChanged (self, event):
619 prop = event.GetProperty()
621 item_section = self.panelProperties.SelectedTreeItem
622 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
623 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
624 config = self.gui.config[plugin]
625 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
626 property_key = prop.GetName()
627 property_value = prop.GetDisplayedString()
629 config[property_section][property_key]['value'] = property_value
631 def OnResultsCheck(self, index, flag):
632 results = self.GetActivePlot().results
633 if results.has_key(self.results_str):
634 results[self.results_str].results[index].visible = flag
635 results[self.results_str].update()
639 def _on_size(self, event):
642 def UpdatePlaylistsTreeSelection(self):
643 playlist = self.GetActivePlaylist()
644 if playlist is not None:
645 if playlist.index >= 0:
646 self._c['status bar'].set_playlist(playlist)
650 def _on_curve_select(self, playlist, curve):
651 #create the plot tab and add playlist to the dictionary
652 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
653 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
654 #tab_index = self._c['notebook'].GetSelection()
655 playlist.figure = plotPanel.get_figure()
656 self.playlists[playlist.name] = playlist
657 #self.playlists[playlist.name] = [playlist, figure]
658 self._c['status bar'].set_playlist(playlist)
663 def _on_playlist_left_doubleclick(self):
664 index = self._c['notebook'].GetSelection()
665 current_playlist = self._c['notebook'].GetPageText(index)
666 if current_playlist != playlist_name:
667 index = self._GetPlaylistTab(playlist_name)
668 self._c['notebook'].SetSelection(index)
669 self._c['status bar'].set_playlist(playlist)
673 def _on_playlist_delete(self, playlist):
674 notebook = self.Parent.plotNotebook
675 index = self.Parent._GetPlaylistTab(playlist.name)
676 notebook.SetSelection(index)
677 notebook.DeletePage(notebook.GetSelection())
678 self.Parent.DeleteFromPlaylists(playlist_name)
682 # Command panel interface
684 def select_command(self, _class, method, command):
685 #self.select_plugin(plugin=command.plugin)
686 self._c['property editor'].clear()
687 self._c['property editor']._argument_from_label = {}
688 for argument in command.arguments:
689 if argument.name == 'help':
692 results = self.execute_command(
693 command=self._command_by_name('playlists'))
694 if not isinstance(results[-1], Success):
695 self._postprocess_text(command, results=results)
698 playlists = results[0]
700 results = self.execute_command(
701 command=self._command_by_name('playlist curves'))
702 if not isinstance(results[-1], Success):
703 self._postprocess_text(command, results=results)
708 ret = props_from_argument(
709 argument, curves=curves, playlists=playlists)
711 continue # property intentionally not handled (yet)
713 self._c['property editor'].append_property(p)
714 self._c['property editor']._argument_from_label[label] = (
717 self.gui.config['selected command'] = command # TODO: push to engine
721 # Note panel interface
723 def _on_update_note(self, _class, method, text):
724 """Sets the note for the active curve.
726 self.execute_command(
727 command=self._command_by_name('set note'),
732 # Playlist panel interface
734 def _on_user_delete_playlist(self, _class, method, playlist):
737 def _on_delete_playlist(self, _class, method, playlist):
738 if hasattr(playlist, 'path') and playlist.path != None:
739 os.remove(playlist.path)
741 def _on_user_delete_curve(self, _class, method, playlist, curve):
744 def _on_delete_curve(self, _class, method, playlist, curve):
745 # TODO: execute_command 'remove curve from playlist'
746 os.remove(curve.path)
748 def _on_set_selected_playlist(self, _class, method, playlist):
749 """Call the `jump to playlist` command.
751 results = self.execute_command(
752 command=self._command_by_name('playlists'))
753 if not isinstance(results[-1], Success):
755 assert len(results) == 2, results
756 playlists = results[0]
757 matching = [p for p in playlists if p.name == playlist.name]
758 assert len(matching) == 1, matching
759 index = playlists.index(matching[0])
760 results = self.execute_command(
761 command=self._command_by_name('jump to playlist'),
762 args={'index':index})
764 def _on_set_selected_curve(self, _class, method, playlist, curve):
765 """Call the `jump to curve` command.
767 self._on_set_selected_playlist(_class, method, playlist)
768 index = playlist.index(curve)
769 results = self.execute_command(
770 command=self._command_by_name('jump to curve'),
771 args={'index':index})
772 if not isinstance(results[-1], Success):
774 #results = self.execute_command(
775 # command=self._command_by_name('get playlist'))
776 #if not isinstance(results[-1], Success):
778 self.execute_command(
779 command=self._command_by_name('get curve'))
783 # Plot panel interface
785 def _on_plot_status_text(self, _class, method, text):
786 if 'status bar' in self._c:
787 self._c['status bar'].set_plot_text(text)
793 def _next_curve(self, *args):
794 """Call the `next curve` command.
796 results = self.execute_command(
797 command=self._command_by_name('next curve'))
798 if isinstance(results[-1], Success):
799 self.execute_command(
800 command=self._command_by_name('get curve'))
802 def _previous_curve(self, *args):
803 """Call the `previous curve` command.
805 results = self.execute_command(
806 command=self._command_by_name('previous curve'))
807 if isinstance(results[-1], Success):
808 self.execute_command(
809 command=self._command_by_name('get curve'))
813 # Panel display handling
815 def _on_panel_visibility(self, _class, method, panel_name, visible):
816 pane = self._c['manager'].GetPane(panel_name)
818 #if we don't do the following, the Folders pane does not resize properly on hide/show
819 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
820 #folders_size = pane.GetSize()
821 self.panelFolders.Fit()
822 self._c['manager'].Update()
824 def _setup_perspectives(self):
825 """Add perspectives to menubar and _perspectives.
827 self._perspectives = {
828 'Default': self._c['manager'].SavePerspective(),
830 path = self.gui.config['perspective path']
831 if os.path.isdir(path):
832 files = sorted(os.listdir(path))
834 name, extension = os.path.splitext(fname)
835 if extension != self.gui.config['perspective extension']:
837 fpath = os.path.join(path, fname)
838 if not os.path.isfile(fpath):
841 with open(fpath, 'rU') as f:
842 perspective = f.readline()
844 self._perspectives[name] = perspective
846 selected_perspective = self.gui.config['active perspective']
847 if not self._perspectives.has_key(selected_perspective):
848 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
850 self._restore_perspective(selected_perspective, force=True)
851 self._update_perspective_menu()
853 def _update_perspective_menu(self):
854 self._c['menu bar']._c['perspective'].update(
855 sorted(self._perspectives.keys()),
856 self.gui.config['active perspective'])
858 def _save_perspective(self, perspective, perspective_dir, name,
860 path = os.path.join(perspective_dir, name)
861 if extension != None:
863 if not os.path.isdir(perspective_dir):
864 os.makedirs(perspective_dir)
865 with open(path, 'w') as f:
867 self._perspectives[name] = perspective
868 self._restore_perspective(name)
869 self._update_perspective_menu()
871 def _delete_perspectives(self, perspective_dir, names,
873 self.log.debug('remove perspectives %s from %s'
874 % (names, perspective_dir))
876 path = os.path.join(perspective_dir, name)
877 if extension != None:
880 del(self._perspectives[name])
881 self._update_perspective_menu()
882 if self.gui.config['active perspective'] in names:
883 self._restore_perspective('Default')
884 # TODO: does this bug still apply?
885 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
886 # http://trac.wxwidgets.org/ticket/3258
887 # ) that makes the radio item indicator in the menu disappear.
888 # The code should be fine once this issue is fixed.
890 def _restore_perspective(self, name, force=False):
891 if name != self.gui.config['active perspective'] or force == True:
892 self.log.debug('restore perspective %s' % name)
893 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
894 self._c['manager'].LoadPerspective(self._perspectives[name])
895 self._c['manager'].Update()
896 for pane in self._c['manager'].GetAllPanes():
897 view = self._c['menu bar']._c['view']
898 if pane.name in view._c.keys():
899 view._c[pane.name].Check(pane.window.IsShown())
901 def _on_save_perspective(self, *args):
902 perspective = self._c['manager'].SavePerspective()
903 name = self.gui.config['active perspective']
904 if name == 'Default':
905 name = 'New perspective'
906 name = select_save_file(
907 directory=self.gui.config['perspective path'],
909 extension=self.gui.config['perspective extension'],
911 message='Enter a name for the new perspective:',
912 caption='Save perspective')
915 self._save_perspective(
916 perspective, self.gui.config['perspective path'], name=name,
917 extension=self.gui.config['perspective extension'])
919 def _on_delete_perspective(self, *args, **kwargs):
920 options = sorted([p for p in self._perspectives.keys()
922 dialog = SelectionDialog(
924 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
925 button_id=wx.ID_DELETE,
926 selection_style='multiple',
928 title='Delete perspective(s)',
929 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
930 dialog.CenterOnScreen()
932 if dialog.canceled == True:
934 names = [options[i] for i in dialog.selected]
936 self._delete_perspectives(
937 self.gui.config['perspective path'], names=names,
938 extension=self.gui.config['perspective extension'])
940 def _on_select_perspective(self, _class, method, name):
941 self._restore_perspective(name)
945 class HookeApp (wx.App):
946 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
948 Tosses up a splash screen and then loads :class:`HookeFrame` in
951 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
953 self.commands = commands
954 self.inqueue = inqueue
955 self.outqueue = outqueue
956 super(HookeApp, self).__init__(*args, **kwargs)
959 self.SetAppName('Hooke')
960 self.SetVendorName('')
961 self._setup_splash_screen()
963 height = self.gui.config['main height']
964 width = self.gui.config['main width']
965 top = self.gui.config['main top']
966 left = self.gui.config['main left']
968 # Sometimes, the ini file gets confused and sets 'left' and
969 # 'top' to large negative numbers. Here we catch and fix
970 # this. Keep small negative numbers, the user might want
979 self.gui, self.commands, self.inqueue, self.outqueue,
980 parent=None, title='Hooke',
981 pos=(left, top), size=(width, height),
982 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
984 self._c['frame'].Show(True)
985 self.SetTopWindow(self._c['frame'])
988 def _setup_splash_screen(self):
989 if self.gui.config['show splash screen'] == True:
990 path = self.gui.config['splash screen image']
991 if os.path.isfile(path):
992 duration = self.gui.config['splash screen duration']
994 bitmap=wx.Image(path).ConvertToBitmap(),
995 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
996 milliseconds=duration,
999 # For some reason splashDuration and sleep do not
1000 # correspond to each other at least not on Windows.
1001 # Maybe it's because duration is in milliseconds and
1002 # sleep in seconds. Thus we need to increase the
1003 # sleep time a bit. A factor of 1.2 seems to work.
1005 time.sleep(sleepFactor * duration / 1000)
1008 class GUI (UserInterface):
1009 """wxWindows graphical user interface.
1012 super(GUI, self).__init__(name='gui')
1014 def default_settings(self):
1015 """Return a list of :class:`hooke.config.Setting`\s for any
1016 configurable UI settings.
1018 The suggested section setting is::
1020 Setting(section=self.setting_section, help=self.__doc__)
1023 Setting(section=self.setting_section, help=self.__doc__),
1024 Setting(section=self.setting_section, option='icon image',
1025 value=os.path.join('doc', 'img', 'microscope.ico'),
1027 help='Path to the hooke icon image.'),
1028 Setting(section=self.setting_section, option='show splash screen',
1029 value=True, type='bool',
1030 help='Enable/disable the splash screen'),
1031 Setting(section=self.setting_section, option='splash screen image',
1032 value=os.path.join('doc', 'img', 'hooke.jpg'),
1034 help='Path to the Hooke splash screen image.'),
1035 Setting(section=self.setting_section,
1036 option='splash screen duration',
1037 value=1000, type='int',
1038 help='Duration of the splash screen in milliseconds.'),
1039 Setting(section=self.setting_section, option='perspective path',
1040 value=os.path.join('resources', 'gui', 'perspective'),
1041 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1042 Setting(section=self.setting_section, option='perspective extension',
1044 help='Extension for perspective files.'),
1045 Setting(section=self.setting_section, option='hide extensions',
1046 value=False, type='bool',
1047 help='Hide file extensions when displaying names.'),
1048 Setting(section=self.setting_section, option='plot legend',
1049 value=True, type='bool',
1050 help='Enable/disable the plot legend.'),
1051 Setting(section=self.setting_section, option='plot SI format',
1052 value='True', type='bool',
1053 help='Enable/disable SI plot axes numbering.'),
1054 Setting(section=self.setting_section, option='plot decimals',
1055 value=2, type='int',
1056 help='Number of decimal places to show if "plot SI format" is enabled.'),
1057 Setting(section=self.setting_section, option='folders-workdir',
1058 value='.', type='path',
1059 help='This should probably go...'),
1060 Setting(section=self.setting_section, option='folders-filters',
1061 value='.', type='path',
1062 help='This should probably go...'),
1063 Setting(section=self.setting_section, option='active perspective',
1065 help='Name of active perspective file (or "Default").'),
1066 Setting(section=self.setting_section,
1067 option='folders-filter-index',
1068 value=0, type='int',
1069 help='This should probably go...'),
1070 Setting(section=self.setting_section, option='main height',
1071 value=450, type='int',
1072 help='Height of main window in pixels.'),
1073 Setting(section=self.setting_section, option='main width',
1074 value=800, type='int',
1075 help='Width of main window in pixels.'),
1076 Setting(section=self.setting_section, option='main top',
1077 value=0, type='int',
1078 help='Pixels from screen top to top of main window.'),
1079 Setting(section=self.setting_section, option='main left',
1080 value=0, type='int',
1081 help='Pixels from screen left to left of main window.'),
1082 Setting(section=self.setting_section, option='selected command',
1083 value='load playlist',
1084 help='Name of the initially selected command.'),
1087 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1091 app = HookeApp(gui=self,
1093 inqueue=ui_to_command_queue,
1094 outqueue=command_to_ui_queue,
1098 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1099 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)