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(command.name, args)
339 self.gui._submit_command(cm, self.inqueue)
342 msg = self.outqueue.get()
344 if isinstance(msg, Exit):
347 elif isinstance(msg, CommandExit):
348 # TODO: display command complete
350 elif isinstance(msg, ReloadUserInterfaceConfig):
351 self.gui.reload_config(msg.config)
353 elif isinstance(msg, Request):
354 h = handler.HANDLERS[msg.type]
355 h.run(self, msg) # TODO: pause for response?
358 self, '_postprocess_%s' % command.name.replace(' ', '_'),
359 self._postprocess_text)
360 pp(command=command, args=args, results=results)
363 def _handle_request(self, msg):
364 """Repeatedly try to get a response to `msg`.
367 raise NotImplementedError('_%s_request_prompt' % msg.type)
368 prompt_string = prompt(msg)
369 parser = getattr(self, '_%s_request_parser' % msg.type, None)
371 raise NotImplementedError('_%s_request_parser' % msg.type)
375 self.cmd.stdout.write(''.join([
376 error.__class__.__name__, ': ', str(error), '\n']))
377 self.cmd.stdout.write(prompt_string)
378 value = parser(msg, self.cmd.stdin.readline())
380 response = msg.response(value)
382 except ValueError, error:
384 self.inqueue.put(response)
388 # Command-specific postprocessing
390 def _postprocess_text(self, command, args={}, results=[]):
391 """Print the string representation of the results to the Results window.
393 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
394 approach, except that :class:`~hooke.ui.commandline.DoCommand`
395 doesn't print some internally handled messages
396 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
398 for result in results:
399 if isinstance(result, CommandExit):
400 self._c['output'].write(result.__class__.__name__+'\n')
401 self._c['output'].write(str(result).rstrip()+'\n')
403 def _postprocess_playlists(self, command, args={}, results=None):
404 """Update `self` to show the playlists.
406 if not isinstance(results[-1], Success):
407 self._postprocess_text(command, results=results)
409 assert len(results) == 2, results
410 playlists = results[0]
411 loaded_playlists = [] # TODO
412 if 'playlist' in self._c:
413 for playlist in playlists:
414 if playlist in loaded_playlists:
415 self._c['playlist'].update_playlist(playlist)
417 self._c['playlist'].add_playlist(playlist)
419 def _postprocess_load_playlist(self, command, args={}, results=None):
420 """Update `self` to show the playlist.
422 if not isinstance(results[-1], Success):
423 self._postprocess_text(command, results=results)
425 assert len(results) == 2, results
426 playlist = results[0]
427 self._c['playlist'].add_playlist(playlist)
429 def _postprocess_get_playlist(self, command, args={}, results=[]):
430 if not isinstance(results[-1], Success):
431 self._postprocess_text(command, results=results)
433 assert len(results) == 2, results
434 playlist = results[0]
435 self._c['playlist'].update_playlist(playlist)
437 def _postprocess_get_curve(self, command, args={}, results=[]):
438 """Update `self` to show the curve.
440 if not isinstance(results[-1], Success):
441 self._postprocess_text(command, results=results)
443 assert len(results) == 2, results
445 if args.get('curve', None) == None:
446 # the command defaults to the current curve of the current playlist
447 results = self.execute_command(
448 command=self._command_by_name('get playlist'))
449 playlist = results[0]
451 raise NotImplementedError()
452 if 'note' in self._c:
453 self._c['note'].set_text(curve.info['note'])
454 if 'playlist' in self._c:
455 self._c['playlist'].set_selected_curve(
457 if 'plot' in self._c:
458 self._c['plot'].set_curve(curve, config=self.gui.config)
460 def _postprocess_next_curve(self, command, args={}, results=[]):
461 """No-op. Only call 'next curve' via `self._next_curve()`.
465 def _postprocess_previous_curve(self, command, args={}, results=[]):
466 """No-op. Only call 'previous curve' via `self._previous_curve()`.
470 def _postprocess_zero_block_surface_contact_point(
471 self, command, args={}, results=[]):
472 """Update the curve, since the available columns may have changed.
474 if isinstance(results[-1], Success):
475 self.execute_command(
476 command=self._command_by_name('get curve'))
478 def _postprocess_add_block_force_array(
479 self, command, args={}, results=[]):
480 """Update the curve, since the available columns may have changed.
482 if isinstance(results[-1], Success):
483 self.execute_command(
484 command=self._command_by_name('get curve'))
490 def _GetActiveFileIndex(self):
491 lib.playlist.Playlist = self.GetActivePlaylist()
492 #get the selected item from the tree
493 selected_item = self._c['playlist']._c['tree'].GetSelection()
494 #test if a playlist or a curve was double-clicked
495 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
499 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
500 while selected_item.IsOk():
502 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
505 def _GetPlaylistTab(self, name):
506 for index, page in enumerate(self._c['notebook']._tabs._pages):
507 if page.caption == name:
511 def select_plugin(self, _class=None, method=None, plugin=None):
514 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
516 playlist = lib.playlist.Playlist(self, self.drivers)
518 playlist.add_curve(item)
519 if playlist.count > 0:
520 playlist.name = self._GetUniquePlaylistName(name)
522 self.AddTayliss(playlist)
524 def AppliesPlotmanipulator(self, name):
526 Returns True if the plotmanipulator 'name' is applied, False otherwise
527 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
529 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
531 def ApplyPlotmanipulators(self, plot, plot_file):
533 Apply all active plotmanipulators.
535 if plot is not None and plot_file is not None:
536 manipulated_plot = copy.deepcopy(plot)
537 for plotmanipulator in self.plotmanipulators:
538 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
539 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
540 return manipulated_plot
542 def GetActiveFigure(self):
543 playlist_name = self.GetActivePlaylistName()
544 figure = self.playlists[playlist_name].figure
545 if figure is not None:
549 def GetActiveFile(self):
550 playlist = self.GetActivePlaylist()
551 if playlist is not None:
552 return playlist.get_active_file()
555 def GetActivePlot(self):
556 playlist = self.GetActivePlaylist()
557 if playlist is not None:
558 return playlist.get_active_file().plot
561 def GetDisplayedPlot(self):
562 plot = copy.deepcopy(self.displayed_plot)
564 #plot.curves = copy.deepcopy(plot.curves)
567 def GetDisplayedPlotCorrected(self):
568 plot = copy.deepcopy(self.displayed_plot)
570 plot.curves = copy.deepcopy(plot.corrected_curves)
573 def GetDisplayedPlotRaw(self):
574 plot = copy.deepcopy(self.displayed_plot)
576 plot.curves = copy.deepcopy(plot.raw_curves)
579 def GetDockArt(self):
580 return self._c['manager'].GetArtProvider()
582 def GetPlotmanipulator(self, name):
584 Returns a plot manipulator function from its name
586 for plotmanipulator in self.plotmanipulators:
587 if plotmanipulator.name == name:
588 return plotmanipulator
591 def HasPlotmanipulator(self, name):
593 returns True if the plotmanipulator 'name' is loaded, False otherwise
595 for plotmanipulator in self.plotmanipulators:
596 if plotmanipulator.command == name:
601 def _on_dir_ctrl_left_double_click(self, event):
602 file_path = self.panelFolders.GetPath()
603 if os.path.isfile(file_path):
604 if file_path.endswith('.hkp'):
605 self.do_loadlist(file_path)
608 def _on_erase_background(self, event):
611 def _on_notebook_page_close(self, event):
612 ctrl = event.GetEventObject()
613 playlist_name = ctrl.GetPageText(ctrl._curpage)
614 self.DeleteFromPlaylists(playlist_name)
616 def OnPaneClose(self, event):
619 def OnPropGridChanged (self, event):
620 prop = event.GetProperty()
622 item_section = self.panelProperties.SelectedTreeItem
623 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
624 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
625 config = self.gui.config[plugin]
626 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
627 property_key = prop.GetName()
628 property_value = prop.GetDisplayedString()
630 config[property_section][property_key]['value'] = property_value
632 def OnResultsCheck(self, index, flag):
633 results = self.GetActivePlot().results
634 if results.has_key(self.results_str):
635 results[self.results_str].results[index].visible = flag
636 results[self.results_str].update()
640 def _on_size(self, event):
643 def UpdatePlaylistsTreeSelection(self):
644 playlist = self.GetActivePlaylist()
645 if playlist is not None:
646 if playlist.index >= 0:
647 self._c['status bar'].set_playlist(playlist)
651 def _on_curve_select(self, playlist, curve):
652 #create the plot tab and add playlist to the dictionary
653 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
654 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
655 #tab_index = self._c['notebook'].GetSelection()
656 playlist.figure = plotPanel.get_figure()
657 self.playlists[playlist.name] = playlist
658 #self.playlists[playlist.name] = [playlist, figure]
659 self._c['status bar'].set_playlist(playlist)
664 def _on_playlist_left_doubleclick(self):
665 index = self._c['notebook'].GetSelection()
666 current_playlist = self._c['notebook'].GetPageText(index)
667 if current_playlist != playlist_name:
668 index = self._GetPlaylistTab(playlist_name)
669 self._c['notebook'].SetSelection(index)
670 self._c['status bar'].set_playlist(playlist)
674 def _on_playlist_delete(self, playlist):
675 notebook = self.Parent.plotNotebook
676 index = self.Parent._GetPlaylistTab(playlist.name)
677 notebook.SetSelection(index)
678 notebook.DeletePage(notebook.GetSelection())
679 self.Parent.DeleteFromPlaylists(playlist_name)
683 # Command panel interface
685 def select_command(self, _class, method, command):
686 #self.select_plugin(plugin=command.plugin)
687 self._c['property editor'].clear()
688 self._c['property editor']._argument_from_label = {}
689 for argument in command.arguments:
690 if argument.name == 'help':
693 results = self.execute_command(
694 command=self._command_by_name('playlists'))
695 if not isinstance(results[-1], Success):
696 self._postprocess_text(command, results=results)
699 playlists = results[0]
701 results = self.execute_command(
702 command=self._command_by_name('playlist curves'))
703 if not isinstance(results[-1], Success):
704 self._postprocess_text(command, results=results)
709 ret = props_from_argument(
710 argument, curves=curves, playlists=playlists)
712 continue # property intentionally not handled (yet)
714 self._c['property editor'].append_property(p)
715 self._c['property editor']._argument_from_label[label] = (
718 self.gui.config['selected command'] = command # TODO: push to engine
722 # Note panel interface
724 def _on_update_note(self, _class, method, text):
725 """Sets the note for the active curve.
727 self.execute_command(
728 command=self._command_by_name('set note'),
733 # Playlist panel interface
735 def _on_user_delete_playlist(self, _class, method, playlist):
738 def _on_delete_playlist(self, _class, method, playlist):
739 if hasattr(playlist, 'path') and playlist.path != None:
740 os.remove(playlist.path)
742 def _on_user_delete_curve(self, _class, method, playlist, curve):
745 def _on_delete_curve(self, _class, method, playlist, curve):
746 # TODO: execute_command 'remove curve from playlist'
747 os.remove(curve.path)
749 def _on_set_selected_playlist(self, _class, method, playlist):
750 """Call the `jump to playlist` command.
752 results = self.execute_command(
753 command=self._command_by_name('playlists'))
754 if not isinstance(results[-1], Success):
756 assert len(results) == 2, results
757 playlists = results[0]
758 matching = [p for p in playlists if p.name == playlist.name]
759 assert len(matching) == 1, matching
760 index = playlists.index(matching[0])
761 results = self.execute_command(
762 command=self._command_by_name('jump to playlist'),
763 args={'index':index})
765 def _on_set_selected_curve(self, _class, method, playlist, curve):
766 """Call the `jump to curve` command.
768 self._on_set_selected_playlist(_class, method, playlist)
769 index = playlist.index(curve)
770 results = self.execute_command(
771 command=self._command_by_name('jump to curve'),
772 args={'index':index})
773 if not isinstance(results[-1], Success):
775 #results = self.execute_command(
776 # command=self._command_by_name('get playlist'))
777 #if not isinstance(results[-1], Success):
779 self.execute_command(
780 command=self._command_by_name('get curve'))
784 # Plot panel interface
786 def _on_plot_status_text(self, _class, method, text):
787 if 'status bar' in self._c:
788 self._c['status bar'].set_plot_text(text)
794 def _next_curve(self, *args):
795 """Call the `next curve` command.
797 results = self.execute_command(
798 command=self._command_by_name('next curve'))
799 if isinstance(results[-1], Success):
800 self.execute_command(
801 command=self._command_by_name('get curve'))
803 def _previous_curve(self, *args):
804 """Call the `previous curve` command.
806 results = self.execute_command(
807 command=self._command_by_name('previous curve'))
808 if isinstance(results[-1], Success):
809 self.execute_command(
810 command=self._command_by_name('get curve'))
814 # Panel display handling
816 def _on_panel_visibility(self, _class, method, panel_name, visible):
817 pane = self._c['manager'].GetPane(panel_name)
819 #if we don't do the following, the Folders pane does not resize properly on hide/show
820 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
821 #folders_size = pane.GetSize()
822 self.panelFolders.Fit()
823 self._c['manager'].Update()
825 def _setup_perspectives(self):
826 """Add perspectives to menubar and _perspectives.
828 self._perspectives = {
829 'Default': self._c['manager'].SavePerspective(),
831 path = self.gui.config['perspective path']
832 if os.path.isdir(path):
833 files = sorted(os.listdir(path))
835 name, extension = os.path.splitext(fname)
836 if extension != self.gui.config['perspective extension']:
838 fpath = os.path.join(path, fname)
839 if not os.path.isfile(fpath):
842 with open(fpath, 'rU') as f:
843 perspective = f.readline()
845 self._perspectives[name] = perspective
847 selected_perspective = self.gui.config['active perspective']
848 if not self._perspectives.has_key(selected_perspective):
849 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
851 self._restore_perspective(selected_perspective, force=True)
852 self._update_perspective_menu()
854 def _update_perspective_menu(self):
855 self._c['menu bar']._c['perspective'].update(
856 sorted(self._perspectives.keys()),
857 self.gui.config['active perspective'])
859 def _save_perspective(self, perspective, perspective_dir, name,
861 path = os.path.join(perspective_dir, name)
862 if extension != None:
864 if not os.path.isdir(perspective_dir):
865 os.makedirs(perspective_dir)
866 with open(path, 'w') as f:
868 self._perspectives[name] = perspective
869 self._restore_perspective(name)
870 self._update_perspective_menu()
872 def _delete_perspectives(self, perspective_dir, names,
874 self.log.debug('remove perspectives %s from %s'
875 % (names, perspective_dir))
877 path = os.path.join(perspective_dir, name)
878 if extension != None:
881 del(self._perspectives[name])
882 self._update_perspective_menu()
883 if self.gui.config['active perspective'] in names:
884 self._restore_perspective('Default')
885 # TODO: does this bug still apply?
886 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
887 # http://trac.wxwidgets.org/ticket/3258
888 # ) that makes the radio item indicator in the menu disappear.
889 # The code should be fine once this issue is fixed.
891 def _restore_perspective(self, name, force=False):
892 if name != self.gui.config['active perspective'] or force == True:
893 self.log.debug('restore perspective %s' % name)
894 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
895 self._c['manager'].LoadPerspective(self._perspectives[name])
896 self._c['manager'].Update()
897 for pane in self._c['manager'].GetAllPanes():
898 view = self._c['menu bar']._c['view']
899 if pane.name in view._c.keys():
900 view._c[pane.name].Check(pane.window.IsShown())
902 def _on_save_perspective(self, *args):
903 perspective = self._c['manager'].SavePerspective()
904 name = self.gui.config['active perspective']
905 if name == 'Default':
906 name = 'New perspective'
907 name = select_save_file(
908 directory=self.gui.config['perspective path'],
910 extension=self.gui.config['perspective extension'],
912 message='Enter a name for the new perspective:',
913 caption='Save perspective')
916 self._save_perspective(
917 perspective, self.gui.config['perspective path'], name=name,
918 extension=self.gui.config['perspective extension'])
920 def _on_delete_perspective(self, *args, **kwargs):
921 options = sorted([p for p in self._perspectives.keys()
923 dialog = SelectionDialog(
925 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
926 button_id=wx.ID_DELETE,
927 selection_style='multiple',
929 title='Delete perspective(s)',
930 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
931 dialog.CenterOnScreen()
933 if dialog.canceled == True:
935 names = [options[i] for i in dialog.selected]
937 self._delete_perspectives(
938 self.gui.config['perspective path'], names=names,
939 extension=self.gui.config['perspective extension'])
941 def _on_select_perspective(self, _class, method, name):
942 self._restore_perspective(name)
946 class HookeApp (wx.App):
947 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
949 Tosses up a splash screen and then loads :class:`HookeFrame` in
952 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
954 self.commands = commands
955 self.inqueue = inqueue
956 self.outqueue = outqueue
957 super(HookeApp, self).__init__(*args, **kwargs)
960 self.SetAppName('Hooke')
961 self.SetVendorName('')
962 self._setup_splash_screen()
964 height = self.gui.config['main height']
965 width = self.gui.config['main width']
966 top = self.gui.config['main top']
967 left = self.gui.config['main left']
969 # Sometimes, the ini file gets confused and sets 'left' and
970 # 'top' to large negative numbers. Here we catch and fix
971 # this. Keep small negative numbers, the user might want
980 self.gui, self.commands, self.inqueue, self.outqueue,
981 parent=None, title='Hooke',
982 pos=(left, top), size=(width, height),
983 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
985 self._c['frame'].Show(True)
986 self.SetTopWindow(self._c['frame'])
989 def _setup_splash_screen(self):
990 if self.gui.config['show splash screen'] == True:
991 path = self.gui.config['splash screen image']
992 if os.path.isfile(path):
993 duration = self.gui.config['splash screen duration']
995 bitmap=wx.Image(path).ConvertToBitmap(),
996 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
997 milliseconds=duration,
1000 # For some reason splashDuration and sleep do not
1001 # correspond to each other at least not on Windows.
1002 # Maybe it's because duration is in milliseconds and
1003 # sleep in seconds. Thus we need to increase the
1004 # sleep time a bit. A factor of 1.2 seems to work.
1006 time.sleep(sleepFactor * duration / 1000)
1009 class GUI (UserInterface):
1010 """wxWindows graphical user interface.
1013 super(GUI, self).__init__(name='gui')
1015 def default_settings(self):
1016 """Return a list of :class:`hooke.config.Setting`\s for any
1017 configurable UI settings.
1019 The suggested section setting is::
1021 Setting(section=self.setting_section, help=self.__doc__)
1024 Setting(section=self.setting_section, help=self.__doc__),
1025 Setting(section=self.setting_section, option='icon image',
1026 value=os.path.join('doc', 'img', 'microscope.ico'),
1028 help='Path to the hooke icon image.'),
1029 Setting(section=self.setting_section, option='show splash screen',
1030 value=True, type='bool',
1031 help='Enable/disable the splash screen'),
1032 Setting(section=self.setting_section, option='splash screen image',
1033 value=os.path.join('doc', 'img', 'hooke.jpg'),
1035 help='Path to the Hooke splash screen image.'),
1036 Setting(section=self.setting_section,
1037 option='splash screen duration',
1038 value=1000, type='int',
1039 help='Duration of the splash screen in milliseconds.'),
1040 Setting(section=self.setting_section, option='perspective path',
1041 value=os.path.join('resources', 'gui', 'perspective'),
1042 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1043 Setting(section=self.setting_section, option='perspective extension',
1045 help='Extension for perspective files.'),
1046 Setting(section=self.setting_section, option='hide extensions',
1047 value=False, type='bool',
1048 help='Hide file extensions when displaying names.'),
1049 Setting(section=self.setting_section, option='plot legend',
1050 value=True, type='bool',
1051 help='Enable/disable the plot legend.'),
1052 Setting(section=self.setting_section, option='plot SI format',
1053 value='True', type='bool',
1054 help='Enable/disable SI plot axes numbering.'),
1055 Setting(section=self.setting_section, option='plot decimals',
1056 value=2, type='int',
1057 help='Number of decimal places to show if "plot SI format" is enabled.'),
1058 Setting(section=self.setting_section, option='folders-workdir',
1059 value='.', type='path',
1060 help='This should probably go...'),
1061 Setting(section=self.setting_section, option='folders-filters',
1062 value='.', type='path',
1063 help='This should probably go...'),
1064 Setting(section=self.setting_section, option='active perspective',
1066 help='Name of active perspective file (or "Default").'),
1067 Setting(section=self.setting_section,
1068 option='folders-filter-index',
1069 value=0, type='int',
1070 help='This should probably go...'),
1071 Setting(section=self.setting_section, option='main height',
1072 value=450, type='int',
1073 help='Height of main window in pixels.'),
1074 Setting(section=self.setting_section, option='main width',
1075 value=800, type='int',
1076 help='Width of main window in pixels.'),
1077 Setting(section=self.setting_section, option='main top',
1078 value=0, type='int',
1079 help='Pixels from screen top to top of main window.'),
1080 Setting(section=self.setting_section, option='main left',
1081 value=0, type='int',
1082 help='Pixels from screen left to left of main window.'),
1083 Setting(section=self.setting_section, option='selected command',
1084 value='load playlist',
1085 help='Name of the initially selected command.'),
1088 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1092 app = HookeApp(gui=self,
1094 inqueue=ui_to_command_queue,
1095 outqueue=command_to_ui_queue,
1099 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1100 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)