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._on_pane_close)
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._set_config('main height', self.GetSize().GetHeight())
273 self._set_config('main left', self.GetPosition()[0])
274 self._set_config('main top', self.GetPosition()[1])
275 self._set_config('main width', self.GetSize().GetWidth())
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.name):
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:
323 if arg.name not in args:
324 continue # undisplayed argument, e.g. 'driver' types.
326 if hasattr(arg, '_display_count'): # support HACK in props_from_argument()
327 count = arg._display_count
328 if count != 1 and arg.name in args:
329 keys = sorted(args[arg.name].keys())
330 assert keys == range(count), keys
331 args[arg.name] = [args[arg.name][i]
332 for i in range(count)]
334 while (len(args[arg.name]) > 0
335 and args[arg.name][-1] == None):
337 if len(args[arg.name]) == 0:
338 args[arg.name] = arg.default
339 cm = CommandMessage(command.name, args)
340 self.gui._submit_command(cm, self.inqueue)
341 return self._handle_response(command_message=cm)
343 def _handle_response(self, command_message):
346 msg = self.outqueue.get()
348 if isinstance(msg, Exit):
351 elif isinstance(msg, CommandExit):
352 # TODO: display command complete
354 elif isinstance(msg, ReloadUserInterfaceConfig):
355 self.gui.reload_config(msg.config)
357 elif isinstance(msg, Request):
358 h = handler.HANDLERS[msg.type]
359 h.run(self, msg) # TODO: pause for response?
362 self, '_postprocess_%s' % command_message.command.replace(' ', '_'),
363 self._postprocess_text)
364 pp(command=command_message.command,
365 args=command_message.arguments,
369 def _handle_request(self, msg):
370 """Repeatedly try to get a response to `msg`.
373 raise NotImplementedError('_%s_request_prompt' % msg.type)
374 prompt_string = prompt(msg)
375 parser = getattr(self, '_%s_request_parser' % msg.type, None)
377 raise NotImplementedError('_%s_request_parser' % msg.type)
381 self.cmd.stdout.write(''.join([
382 error.__class__.__name__, ': ', str(error), '\n']))
383 self.cmd.stdout.write(prompt_string)
384 value = parser(msg, self.cmd.stdin.readline())
386 response = msg.response(value)
388 except ValueError, error:
390 self.inqueue.put(response)
392 def _set_config(self, option, value, section=None):
393 self.gui._set_config(section=section, option=option, value=value,
394 ui_to_command_queue=self.inqueue,
395 response_handler=self._handle_response)
398 # Command-specific postprocessing
400 def _postprocess_text(self, command, args={}, results=[]):
401 """Print the string representation of the results to the Results window.
403 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
404 approach, except that :class:`~hooke.ui.commandline.DoCommand`
405 doesn't print some internally handled messages
406 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
408 for result in results:
409 if isinstance(result, CommandExit):
410 self._c['output'].write(result.__class__.__name__+'\n')
411 self._c['output'].write(str(result).rstrip()+'\n')
413 def _postprocess_playlists(self, command, args={}, results=None):
414 """Update `self` to show the playlists.
416 if not isinstance(results[-1], Success):
417 self._postprocess_text(command, results=results)
419 assert len(results) == 2, results
420 playlists = results[0]
421 if 'playlist' in self._c:
422 for playlist in playlists:
423 if self._c['playlist'].is_playlist_loaded(playlist):
424 self._c['playlist'].update_playlist(playlist)
426 self._c['playlist'].add_playlist(playlist)
428 def _postprocess_new_playlist(self, command, args={}, results=None):
429 """Update `self` to show the new playlist.
431 if not isinstance(results[-1], Success):
432 self._postprocess_text(command, results=results)
434 assert len(results) == 2, results
435 playlist = results[0]
436 if 'playlist' in self._c:
437 loaded = self._c['playlist'].is_playlist_loaded(playlist)
438 assert loaded == False, loaded
439 self._c['playlist'].add_playlist(playlist)
441 def _postprocess_load_playlist(self, command, args={}, results=None):
442 """Update `self` to show the playlist.
444 if not isinstance(results[-1], Success):
445 self._postprocess_text(command, results=results)
447 assert len(results) == 2, results
448 playlist = results[0]
449 self._c['playlist'].add_playlist(playlist)
451 def _postprocess_get_playlist(self, command, args={}, results=[]):
452 if not isinstance(results[-1], Success):
453 self._postprocess_text(command, results=results)
455 assert len(results) == 2, results
456 playlist = results[0]
457 if 'playlist' in self._c:
458 loaded = self._c['playlist'].is_playlist_loaded(playlist)
459 assert loaded == True, loaded
460 self._c['playlist'].update_playlist(playlist)
462 def _postprocess_get_curve(self, command, args={}, results=[]):
463 """Update `self` to show the curve.
465 if not isinstance(results[-1], Success):
466 self._postprocess_text(command, results=results)
468 assert len(results) == 2, results
470 if args.get('curve', None) == None:
471 # the command defaults to the current curve of the current playlist
472 results = self.execute_command(
473 command=self._command_by_name('get playlist'))
474 playlist = results[0]
476 raise NotImplementedError()
477 if 'note' in self._c:
478 self._c['note'].set_text(curve.info.get('note', ''))
479 if 'playlist' in self._c:
480 self._c['playlist'].set_selected_curve(
482 if 'plot' in self._c:
483 self._c['plot'].set_curve(curve, config=self.gui.config)
485 def _postprocess_next_curve(self, command, args={}, results=[]):
486 """No-op. Only call 'next curve' via `self._next_curve()`.
490 def _postprocess_previous_curve(self, command, args={}, results=[]):
491 """No-op. Only call 'previous curve' via `self._previous_curve()`.
495 def _postprocess_glob_curves_to_playlist(
496 self, command, args={}, results=[]):
497 """Update `self` to show new curves.
499 if not isinstance(results[-1], Success):
500 self._postprocess_text(command, results=results)
502 if 'playlist' in self._c:
503 if args.get('playlist', None) != None:
504 playlist = args['playlist']
505 pname = playlist.name
506 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
507 assert loaded == True, loaded
508 for curve in results[:-1]:
509 self._c['playlist']._add_curve(pname, curve)
511 self.execute_command(
512 command=self._command_by_name('get playlist'))
514 def _postprocess_zero_block_surface_contact_point(
515 self, command, args={}, results=[]):
516 """Update the curve, since the available columns may have changed.
518 if isinstance(results[-1], Success):
519 self.execute_command(
520 command=self._command_by_name('get curve'))
522 def _postprocess_add_block_force_array(
523 self, command, args={}, results=[]):
524 """Update the curve, since the available columns may have changed.
526 if isinstance(results[-1], Success):
527 self.execute_command(
528 command=self._command_by_name('get curve'))
534 def _GetActiveFileIndex(self):
535 lib.playlist.Playlist = self.GetActivePlaylist()
536 #get the selected item from the tree
537 selected_item = self._c['playlist']._c['tree'].GetSelection()
538 #test if a playlist or a curve was double-clicked
539 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
543 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
544 while selected_item.IsOk():
546 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
549 def _GetPlaylistTab(self, name):
550 for index, page in enumerate(self._c['notebook']._tabs._pages):
551 if page.caption == name:
555 def select_plugin(self, _class=None, method=None, plugin=None):
558 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
560 playlist = lib.playlist.Playlist(self, self.drivers)
562 playlist.add_curve(item)
563 if playlist.count > 0:
564 playlist.name = self._GetUniquePlaylistName(name)
566 self.AddTayliss(playlist)
568 def AppliesPlotmanipulator(self, name):
570 Returns True if the plotmanipulator 'name' is applied, False otherwise
571 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
573 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
575 def ApplyPlotmanipulators(self, plot, plot_file):
577 Apply all active plotmanipulators.
579 if plot is not None and plot_file is not None:
580 manipulated_plot = copy.deepcopy(plot)
581 for plotmanipulator in self.plotmanipulators:
582 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
583 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
584 return manipulated_plot
586 def GetActiveFigure(self):
587 playlist_name = self.GetActivePlaylistName()
588 figure = self.playlists[playlist_name].figure
589 if figure is not None:
593 def GetActiveFile(self):
594 playlist = self.GetActivePlaylist()
595 if playlist is not None:
596 return playlist.get_active_file()
599 def GetActivePlot(self):
600 playlist = self.GetActivePlaylist()
601 if playlist is not None:
602 return playlist.get_active_file().plot
605 def GetDisplayedPlot(self):
606 plot = copy.deepcopy(self.displayed_plot)
608 #plot.curves = copy.deepcopy(plot.curves)
611 def GetDisplayedPlotCorrected(self):
612 plot = copy.deepcopy(self.displayed_plot)
614 plot.curves = copy.deepcopy(plot.corrected_curves)
617 def GetDisplayedPlotRaw(self):
618 plot = copy.deepcopy(self.displayed_plot)
620 plot.curves = copy.deepcopy(plot.raw_curves)
623 def GetDockArt(self):
624 return self._c['manager'].GetArtProvider()
626 def GetPlotmanipulator(self, name):
628 Returns a plot manipulator function from its name
630 for plotmanipulator in self.plotmanipulators:
631 if plotmanipulator.name == name:
632 return plotmanipulator
635 def HasPlotmanipulator(self, name):
637 returns True if the plotmanipulator 'name' is loaded, False otherwise
639 for plotmanipulator in self.plotmanipulators:
640 if plotmanipulator.command == name:
645 def _on_dir_ctrl_left_double_click(self, event):
646 file_path = self.panelFolders.GetPath()
647 if os.path.isfile(file_path):
648 if file_path.endswith('.hkp'):
649 self.do_loadlist(file_path)
652 def _on_erase_background(self, event):
655 def _on_notebook_page_close(self, event):
656 ctrl = event.GetEventObject()
657 playlist_name = ctrl.GetPageText(ctrl._curpage)
658 self.DeleteFromPlaylists(playlist_name)
660 def OnPropGridChanged (self, event):
661 prop = event.GetProperty()
663 item_section = self.panelProperties.SelectedTreeItem
664 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
665 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
666 config = self.gui.config[plugin]
667 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
668 property_key = prop.GetName()
669 property_value = prop.GetDisplayedString()
671 config[property_section][property_key]['value'] = property_value
673 def OnResultsCheck(self, index, flag):
674 results = self.GetActivePlot().results
675 if results.has_key(self.results_str):
676 results[self.results_str].results[index].visible = flag
677 results[self.results_str].update()
681 def _on_size(self, event):
684 def UpdatePlaylistsTreeSelection(self):
685 playlist = self.GetActivePlaylist()
686 if playlist is not None:
687 if playlist.index >= 0:
688 self._c['status bar'].set_playlist(playlist)
692 def _on_curve_select(self, playlist, curve):
693 #create the plot tab and add playlist to the dictionary
694 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
695 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
696 #tab_index = self._c['notebook'].GetSelection()
697 playlist.figure = plotPanel.get_figure()
698 self.playlists[playlist.name] = playlist
699 #self.playlists[playlist.name] = [playlist, figure]
700 self._c['status bar'].set_playlist(playlist)
705 def _on_playlist_left_doubleclick(self):
706 index = self._c['notebook'].GetSelection()
707 current_playlist = self._c['notebook'].GetPageText(index)
708 if current_playlist != playlist_name:
709 index = self._GetPlaylistTab(playlist_name)
710 self._c['notebook'].SetSelection(index)
711 self._c['status bar'].set_playlist(playlist)
715 def _on_playlist_delete(self, playlist):
716 notebook = self.Parent.plotNotebook
717 index = self.Parent._GetPlaylistTab(playlist.name)
718 notebook.SetSelection(index)
719 notebook.DeletePage(notebook.GetSelection())
720 self.Parent.DeleteFromPlaylists(playlist_name)
724 # Command panel interface
726 def select_command(self, _class, method, command):
727 #self.select_plugin(plugin=command.plugin)
728 self._c['property editor'].clear()
729 self._c['property editor']._argument_from_label = {}
730 for argument in command.arguments:
731 if argument.name == 'help':
734 results = self.execute_command(
735 command=self._command_by_name('playlists'))
736 if not isinstance(results[-1], Success):
737 self._postprocess_text(command, results=results)
740 playlists = results[0]
742 results = self.execute_command(
743 command=self._command_by_name('playlist curves'))
744 if not isinstance(results[-1], Success):
745 self._postprocess_text(command, results=results)
750 ret = props_from_argument(
751 argument, curves=curves, playlists=playlists)
753 continue # property intentionally not handled (yet)
755 self._c['property editor'].append_property(p)
756 self._c['property editor']._argument_from_label[label] = (
759 self._set_config('selected command', command.name)
763 # Note panel interface
765 def _on_update_note(self, _class, method, text):
766 """Sets the note for the active curve.
768 self.execute_command(
769 command=self._command_by_name('set note'),
774 # Playlist panel interface
776 def _on_user_delete_playlist(self, _class, method, playlist):
779 def _on_delete_playlist(self, _class, method, playlist):
780 if hasattr(playlist, 'path') and playlist.path != None:
781 os.remove(playlist.path)
783 def _on_user_delete_curve(self, _class, method, playlist, curve):
786 def _on_delete_curve(self, _class, method, playlist, curve):
787 # TODO: execute_command 'remove curve from playlist'
788 os.remove(curve.path)
790 def _on_set_selected_playlist(self, _class, method, playlist):
791 """Call the `jump to playlist` command.
793 results = self.execute_command(
794 command=self._command_by_name('playlists'))
795 if not isinstance(results[-1], Success):
797 assert len(results) == 2, results
798 playlists = results[0]
799 matching = [p for p in playlists if p.name == playlist.name]
800 assert len(matching) == 1, matching
801 index = playlists.index(matching[0])
802 results = self.execute_command(
803 command=self._command_by_name('jump to playlist'),
804 args={'index':index})
806 def _on_set_selected_curve(self, _class, method, playlist, curve):
807 """Call the `jump to curve` command.
809 self._on_set_selected_playlist(_class, method, playlist)
810 index = playlist.index(curve)
811 results = self.execute_command(
812 command=self._command_by_name('jump to curve'),
813 args={'index':index})
814 if not isinstance(results[-1], Success):
816 #results = self.execute_command(
817 # command=self._command_by_name('get playlist'))
818 #if not isinstance(results[-1], Success):
820 self.execute_command(
821 command=self._command_by_name('get curve'))
825 # Plot panel interface
827 def _on_plot_status_text(self, _class, method, text):
828 if 'status bar' in self._c:
829 self._c['status bar'].set_plot_text(text)
835 def _next_curve(self, *args):
836 """Call the `next curve` command.
838 results = self.execute_command(
839 command=self._command_by_name('next curve'))
840 if isinstance(results[-1], Success):
841 self.execute_command(
842 command=self._command_by_name('get curve'))
844 def _previous_curve(self, *args):
845 """Call the `previous curve` command.
847 results = self.execute_command(
848 command=self._command_by_name('previous curve'))
849 if isinstance(results[-1], Success):
850 self.execute_command(
851 command=self._command_by_name('get curve'))
855 # Panel display handling
857 def _on_pane_close(self, event):
859 view = self._c['menu bar']._c['view']
860 if pane.name in view._c.keys():
861 view._c[pane.name].Check(False)
864 def _on_panel_visibility(self, _class, method, panel_name, visible):
865 pane = self._c['manager'].GetPane(panel_name)
867 #if we don't do the following, the Folders pane does not resize properly on hide/show
868 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
869 #folders_size = pane.GetSize()
870 self.panelFolders.Fit()
871 self._c['manager'].Update()
873 def _setup_perspectives(self):
874 """Add perspectives to menubar and _perspectives.
876 self._perspectives = {
877 'Default': self._c['manager'].SavePerspective(),
879 path = self.gui.config['perspective path']
880 if os.path.isdir(path):
881 files = sorted(os.listdir(path))
883 name, extension = os.path.splitext(fname)
884 if extension != self.gui.config['perspective extension']:
886 fpath = os.path.join(path, fname)
887 if not os.path.isfile(fpath):
890 with open(fpath, 'rU') as f:
891 perspective = f.readline()
893 self._perspectives[name] = perspective
895 selected_perspective = self.gui.config['active perspective']
896 if not self._perspectives.has_key(selected_perspective):
897 self._set_config('active perspective', 'Default')
899 self._restore_perspective(selected_perspective, force=True)
900 self._update_perspective_menu()
902 def _update_perspective_menu(self):
903 self._c['menu bar']._c['perspective'].update(
904 sorted(self._perspectives.keys()),
905 self.gui.config['active perspective'])
907 def _save_perspective(self, perspective, perspective_dir, name,
909 path = os.path.join(perspective_dir, name)
910 if extension != None:
912 if not os.path.isdir(perspective_dir):
913 os.makedirs(perspective_dir)
914 with open(path, 'w') as f:
916 self._perspectives[name] = perspective
917 self._restore_perspective(name)
918 self._update_perspective_menu()
920 def _delete_perspectives(self, perspective_dir, names,
922 self.log.debug('remove perspectives %s from %s'
923 % (names, perspective_dir))
925 path = os.path.join(perspective_dir, name)
926 if extension != None:
929 del(self._perspectives[name])
930 self._update_perspective_menu()
931 if self.gui.config['active perspective'] in names:
932 self._restore_perspective('Default')
933 # TODO: does this bug still apply?
934 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
935 # http://trac.wxwidgets.org/ticket/3258
936 # ) that makes the radio item indicator in the menu disappear.
937 # The code should be fine once this issue is fixed.
939 def _restore_perspective(self, name, force=False):
940 if name != self.gui.config['active perspective'] or force == True:
941 self.log.debug('restore perspective %s' % name)
942 self._set_config('active perspective', name)
943 self._c['manager'].LoadPerspective(self._perspectives[name])
944 self._c['manager'].Update()
945 for pane in self._c['manager'].GetAllPanes():
946 view = self._c['menu bar']._c['view']
947 if pane.name in view._c.keys():
948 view._c[pane.name].Check(pane.window.IsShown())
950 def _on_save_perspective(self, *args):
951 perspective = self._c['manager'].SavePerspective()
952 name = self.gui.config['active perspective']
953 if name == 'Default':
954 name = 'New perspective'
955 name = select_save_file(
956 directory=self.gui.config['perspective path'],
958 extension=self.gui.config['perspective extension'],
960 message='Enter a name for the new perspective:',
961 caption='Save perspective')
964 self._save_perspective(
965 perspective, self.gui.config['perspective path'], name=name,
966 extension=self.gui.config['perspective extension'])
968 def _on_delete_perspective(self, *args, **kwargs):
969 options = sorted([p for p in self._perspectives.keys()
971 dialog = SelectionDialog(
973 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
974 button_id=wx.ID_DELETE,
975 selection_style='multiple',
977 title='Delete perspective(s)',
978 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
979 dialog.CenterOnScreen()
981 if dialog.canceled == True:
983 names = [options[i] for i in dialog.selected]
985 self._delete_perspectives(
986 self.gui.config['perspective path'], names=names,
987 extension=self.gui.config['perspective extension'])
989 def _on_select_perspective(self, _class, method, name):
990 self._restore_perspective(name)
994 class HookeApp (wx.App):
995 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
997 Tosses up a splash screen and then loads :class:`HookeFrame` in
1000 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
1002 self.commands = commands
1003 self.inqueue = inqueue
1004 self.outqueue = outqueue
1005 super(HookeApp, self).__init__(*args, **kwargs)
1008 self.SetAppName('Hooke')
1009 self.SetVendorName('')
1010 self._setup_splash_screen()
1012 height = self.gui.config['main height']
1013 width = self.gui.config['main width']
1014 top = self.gui.config['main top']
1015 left = self.gui.config['main left']
1017 # Sometimes, the ini file gets confused and sets 'left' and
1018 # 'top' to large negative numbers. Here we catch and fix
1019 # this. Keep small negative numbers, the user might want
1027 'frame': HookeFrame(
1028 self.gui, self.commands, self.inqueue, self.outqueue,
1029 parent=None, title='Hooke',
1030 pos=(left, top), size=(width, height),
1031 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
1033 self._c['frame'].Show(True)
1034 self.SetTopWindow(self._c['frame'])
1037 def _setup_splash_screen(self):
1038 if self.gui.config['show splash screen'] == True:
1039 path = self.gui.config['splash screen image']
1040 if os.path.isfile(path):
1041 duration = self.gui.config['splash screen duration']
1043 bitmap=wx.Image(path).ConvertToBitmap(),
1044 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
1045 milliseconds=duration,
1048 # For some reason splashDuration and sleep do not
1049 # correspond to each other at least not on Windows.
1050 # Maybe it's because duration is in milliseconds and
1051 # sleep in seconds. Thus we need to increase the
1052 # sleep time a bit. A factor of 1.2 seems to work.
1054 time.sleep(sleepFactor * duration / 1000)
1057 class GUI (UserInterface):
1058 """wxWindows graphical user interface.
1061 super(GUI, self).__init__(name='gui')
1063 def default_settings(self):
1064 """Return a list of :class:`hooke.config.Setting`\s for any
1065 configurable UI settings.
1067 The suggested section setting is::
1069 Setting(section=self.setting_section, help=self.__doc__)
1072 Setting(section=self.setting_section, help=self.__doc__),
1073 Setting(section=self.setting_section, option='icon image',
1074 value=os.path.join('doc', 'img', 'microscope.ico'),
1076 help='Path to the hooke icon image.'),
1077 Setting(section=self.setting_section, option='show splash screen',
1078 value=True, type='bool',
1079 help='Enable/disable the splash screen'),
1080 Setting(section=self.setting_section, option='splash screen image',
1081 value=os.path.join('doc', 'img', 'hooke.jpg'),
1083 help='Path to the Hooke splash screen image.'),
1084 Setting(section=self.setting_section,
1085 option='splash screen duration',
1086 value=1000, type='int',
1087 help='Duration of the splash screen in milliseconds.'),
1088 Setting(section=self.setting_section, option='perspective path',
1089 value=os.path.join('resources', 'gui', 'perspective'),
1090 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1091 Setting(section=self.setting_section, option='perspective extension',
1093 help='Extension for perspective files.'),
1094 Setting(section=self.setting_section, option='hide extensions',
1095 value=False, type='bool',
1096 help='Hide file extensions when displaying names.'),
1097 Setting(section=self.setting_section, option='plot legend',
1098 value=True, type='bool',
1099 help='Enable/disable the plot legend.'),
1100 Setting(section=self.setting_section, option='plot SI format',
1101 value='True', type='bool',
1102 help='Enable/disable SI plot axes numbering.'),
1103 Setting(section=self.setting_section, option='plot decimals',
1104 value=2, type='int',
1105 help='Number of decimal places to show if "plot SI format" is enabled.'),
1106 Setting(section=self.setting_section, option='folders-workdir',
1107 value='.', type='path',
1108 help='This should probably go...'),
1109 Setting(section=self.setting_section, option='folders-filters',
1110 value='.', type='path',
1111 help='This should probably go...'),
1112 Setting(section=self.setting_section, option='active perspective',
1114 help='Name of active perspective file (or "Default").'),
1115 Setting(section=self.setting_section,
1116 option='folders-filter-index',
1117 value=0, type='int',
1118 help='This should probably go...'),
1119 Setting(section=self.setting_section, option='main height',
1120 value=450, type='int',
1121 help='Height of main window in pixels.'),
1122 Setting(section=self.setting_section, option='main width',
1123 value=800, type='int',
1124 help='Width of main window in pixels.'),
1125 Setting(section=self.setting_section, option='main top',
1126 value=0, type='int',
1127 help='Pixels from screen top to top of main window.'),
1128 Setting(section=self.setting_section, option='main left',
1129 value=0, type='int',
1130 help='Pixels from screen left to left of main window.'),
1131 Setting(section=self.setting_section, option='selected command',
1132 value='load playlist',
1133 help='Name of the initially selected command.'),
1136 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1140 app = HookeApp(gui=self,
1142 inqueue=ui_to_command_queue,
1143 outqueue=command_to_ui_queue,
1147 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1148 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)