3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
\r
10 wxversion.select(WX_GOOD)
\r
21 import wx.aui as aui
\r
22 import wx.lib.evtmgr as evtmgr
\r
23 # wxPropertyGrid is included in wxPython >= 2.9.1, see
\r
24 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
\r
25 # until then, we'll avoid it because of the *nix build problems.
\r
26 #import wx.propgrid as wxpg
\r
28 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
\r
29 from ...config import Setting
\r
30 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
31 from ...ui import UserInterface, CommandMessage
\r
32 from .dialog.selection import Selection as SelectionDialog
\r
33 from .dialog.save_file import select_save_file
\r
34 from . import menu as menu
\r
35 from . import navbar as navbar
\r
36 from . import panel as panel
\r
37 from .panel.propertyeditor import prop_from_argument, prop_from_setting
\r
38 from . import statusbar as statusbar
\r
41 class HookeFrame (wx.Frame):
\r
42 """The main Hooke-interface window.
\r
44 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
45 super(HookeFrame, self).__init__(*args, **kwargs)
\r
46 self.log = logging.getLogger('hooke')
\r
48 self.commands = commands
\r
49 self.inqueue = inqueue
\r
50 self.outqueue = outqueue
\r
51 self._perspectives = {} # {name: perspective_str}
\r
54 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
56 # setup frame manager
\r
57 self._c['manager'] = aui.AuiManager()
\r
58 self._c['manager'].SetManagedWindow(self)
\r
60 # set the gradient and drag styles
\r
61 self._c['manager'].GetArtProvider().SetMetric(
\r
62 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
63 self._c['manager'].SetFlags(
\r
64 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
66 # Min size for the frame itself isn't completely done. See
\r
67 # the end of FrameManager::Update() for the test code. For
\r
68 # now, just hard code a frame minimum size.
\r
69 #self.SetMinSize(wx.Size(500, 500))
\r
71 self._setup_panels()
\r
72 self._setup_toolbars()
\r
73 self._c['manager'].Update() # commit pending changes
\r
75 # Create the menubar after the panes so that the default
\r
76 # perspective is created with all panes open
\r
77 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
\r
78 self._c['menu bar'] = menu.HookeMenuBar(
\r
82 'close': self._on_close,
\r
83 'about': self._on_about,
\r
84 'view_panel': self._on_panel_visibility,
\r
85 'save_perspective': self._on_save_perspective,
\r
86 'delete_perspective': self._on_delete_perspective,
\r
87 'select_perspective': self._on_select_perspective,
\r
89 self.SetMenuBar(self._c['menu bar'])
\r
91 self._c['status bar'] = statusbar.StatusBar(
\r
93 style=wx.ST_SIZEGRIP)
\r
94 self.SetStatusBar(self._c['status bar'])
\r
96 self._setup_perspectives()
\r
99 self.execute_command(
\r
100 command=self._command_by_name('load playlist'),
\r
101 args={'input':'test/data/vclamp_picoforce/playlist'},
\r
103 return # TODO: cleanup
\r
104 self.playlists = self._c['playlist'].Playlists
\r
105 self._displayed_plot = None
\r
106 #load default list, if possible
\r
107 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
\r
112 def _setup_panels(self):
\r
113 client_size = self.GetClientSize()
\r
115 # ('folders', wx.GenericDirCtrl(
\r
117 # dir=self.gui.config['folders-workdir'],
\r
119 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
120 # filter=self.gui.config['folders-filters'],
\r
121 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
122 (panel.PANELS['playlist'](
\r
124 'delete_playlist':self._on_user_delete_playlist,
\r
125 '_delete_playlist':self._on_delete_playlist,
\r
126 'delete_curve':self._on_user_delete_curve,
\r
127 '_delete_curve':self._on_delete_curve,
\r
128 '_on_set_selected_playlist':self._on_set_selected_playlist,
\r
129 '_on_set_selected_curve':self._on_set_selected_curve,
\r
132 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
133 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
136 (panel.PANELS['note'](
\r
138 '_on_update':self._on_update_note,
\r
141 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
144 # ('notebook', Notebook(
\r
146 # pos=wx.Point(client_size.x, client_size.y),
\r
147 # size=wx.Size(430, 200),
\r
148 # style=aui.AUI_NB_DEFAULT_STYLE
\r
149 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
150 (panel.PANELS['commands'](
\r
151 commands=self.commands,
\r
152 selected=self.gui.config['selected command'],
\r
154 'execute': self.execute_command,
\r
155 'select_plugin': self.select_plugin,
\r
156 'select_command': self.select_command,
\r
157 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
160 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
161 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
164 (panel.PANELS['propertyeditor'](
\r
167 style=wx.WANTS_CHARS,
\r
168 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
170 # ('assistant', wx.TextCtrl(
\r
172 # pos=wx.Point(0, 0),
\r
173 # size=wx.Size(150, 90),
\r
174 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
175 (panel.PANELS['plot'](
\r
179 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
180 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
183 (panel.PANELS['output'](
\r
185 pos=wx.Point(0, 0),
\r
186 size=wx.Size(150, 90),
\r
187 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
189 # ('results', panel.results.Results(self), 'bottom'),
\r
191 self._add_panel(p, style)
\r
192 #self._c['assistant'].SetEditable(False)
\r
194 def _add_panel(self, panel, style):
\r
195 self._c[panel.name] = panel
\r
196 m_name = panel.managed_name
\r
197 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
\r
198 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
201 elif style == 'center':
\r
203 elif style == 'left':
\r
205 elif style == 'right':
\r
208 assert style == 'bottom', style
\r
210 self._c['manager'].AddPane(panel, info)
\r
212 def _setup_toolbars(self):
\r
213 self._c['navigation bar'] = navbar.NavBar(
\r
215 'next': self._next_curve,
\r
216 'previous': self._previous_curve,
\r
219 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
220 self._c['manager'].AddPane(
\r
221 self._c['navigation bar'],
\r
222 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
223 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
224 ).RightDockable(False))
\r
226 def _bind_events(self):
\r
227 # TODO: figure out if we can use the eventManager for menu
\r
228 # ranges and events of 'self' without raising an assertion
\r
230 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
231 self.Bind(wx.EVT_SIZE, self._on_size)
\r
232 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
233 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
234 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
236 return # TODO: cleanup
\r
237 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
238 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
241 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
243 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
245 def _on_about(self, *args):
\r
246 dialog = wx.MessageDialog(
\r
248 message=self.gui._splash_text(extra_info={
\r
249 'get-details':'click "Help -> License"'},
\r
251 caption='About Hooke',
\r
252 style=wx.OK|wx.ICON_INFORMATION)
\r
256 def _on_close(self, *args):
\r
257 self.log.info('closing GUI framework')
\r
259 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
260 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
261 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
262 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
263 # push changes back to Hooke.config?
\r
264 self._c['manager'].UnInit()
\r
265 del self._c['manager']
\r
270 # Panel utility functions
\r
272 def _file_name(self, name):
\r
273 """Cleanup names according to configured preferences.
\r
275 if self.gui.config['hide extensions'] == 'True': # HACK: config should decode
\r
276 name,ext = os.path.splitext(name)
\r
283 def _command_by_name(self, name):
\r
284 cs = [c for c in self.commands if c.name == name]
\r
286 raise KeyError(name)
\r
288 raise Exception('Multiple commands named "%s"' % name)
\r
291 def execute_command(self, _class=None, method=None,
\r
292 command=None, args=None):
\r
295 if ('property editor' in self._c
\r
296 and self.gui.config['selected command'] == command):
\r
297 arg_names = [arg.name for arg in command.arguments]
\r
298 for name,value in self._c['property editor'].get_values().items():
\r
299 if name in arg_names:
\r
301 self.log.debug('executing %s with %s' % (command.name, args))
\r
302 self.inqueue.put(CommandMessage(command, args))
\r
305 msg = self.outqueue.get()
\r
306 results.append(msg)
\r
307 if isinstance(msg, Exit):
\r
310 elif isinstance(msg, CommandExit):
\r
311 # TODO: display command complete
\r
313 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
314 self.gui.reload_config(msg.config)
\r
316 elif isinstance(msg, Request):
\r
317 h = handler.HANDLERS[msg.type]
\r
318 h.run(self, msg) # TODO: pause for response?
\r
321 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
322 self._postprocess_text)
\r
323 pp(command=command, args=args, results=results)
\r
326 def _handle_request(self, msg):
\r
327 """Repeatedly try to get a response to `msg`.
\r
330 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
331 prompt_string = prompt(msg)
\r
332 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
334 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
338 self.cmd.stdout.write(''.join([
\r
339 error.__class__.__name__, ': ', str(error), '\n']))
\r
340 self.cmd.stdout.write(prompt_string)
\r
341 value = parser(msg, self.cmd.stdin.readline())
\r
343 response = msg.response(value)
\r
345 except ValueError, error:
\r
347 self.inqueue.put(response)
\r
351 # Command-specific postprocessing
\r
353 def _postprocess_text(self, command, args={}, results=[]):
\r
354 """Print the string representation of the results to the Results window.
\r
356 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
357 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
358 doesn't print some internally handled messages
\r
359 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
361 for result in results:
\r
362 if isinstance(result, CommandExit):
\r
363 self._c['output'].write(result.__class__.__name__+'\n')
\r
364 self._c['output'].write(str(result).rstrip()+'\n')
\r
366 def _postprocess_load_playlist(self, command, args={}, results=None):
\r
367 """Update `self` to show the playlist.
\r
369 if not isinstance(results[-1], Success):
\r
370 self._postprocess_text(command, results=results)
\r
372 assert len(results) == 2, results
\r
373 playlist = results[0]
\r
374 self._c['playlist']._c['tree'].add_playlist(playlist)
\r
376 def _postprocess_get_playlist(self, command, args={}, results=[]):
\r
377 if not isinstance(results[-1], Success):
\r
378 self._postprocess_text(command, results=results)
\r
380 assert len(results) == 2, results
\r
381 playlist = results[0]
\r
382 self._c['playlist']._c['tree'].update_playlist(playlist)
\r
384 def _postprocess_get_curve(self, command, args={}, results=[]):
\r
385 """Update `self` to show the curve.
\r
387 if not isinstance(results[-1], Success):
\r
388 self._postprocess_text(command, results=results)
\r
390 assert len(results) == 2, results
\r
392 if args.get('curve', None) == None:
\r
393 # the command defaults to the current curve of the current playlist
\r
394 results = self.execute_command(
\r
395 command=self._command_by_name('get playlist'))
\r
396 playlist = results[0]
\r
398 raise NotImplementedError()
\r
399 if 'note' in self._c:
\r
400 print sorted(curve.info.keys())
\r
401 self._c['note'].set_text(curve.info['note'])
\r
402 if 'playlist' in self._c:
\r
403 self._c['playlist']._c['tree'].set_selected_curve(
\r
405 if 'plot' in self._c:
\r
406 self._c['plot'].set_curve(curve, config=self.gui.config)
\r
408 def _postprocess_next_curve(self, command, args={}, results=[]):
\r
409 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
413 def _postprocess_previous_curve(self, command, args={}, results=[]):
\r
414 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
418 def _postprocess_zero_block_surface_contact_point(
\r
419 self, command, args={}, results=[]):
\r
420 """Update the curve, since the available columns may have changed.
\r
422 if isinstance(results[-1], Success):
\r
423 self.execute_command(
\r
424 command=self._command_by_name('get curve'))
\r
426 def _postprocess_add_block_force_array(
\r
427 self, command, args={}, results=[]):
\r
428 """Update the curve, since the available columns may have changed.
\r
430 if isinstance(results[-1], Success):
\r
431 self.execute_command(
\r
432 command=self._command_by_name('get curve'))
\r
438 def _GetActiveFileIndex(self):
\r
439 lib.playlist.Playlist = self.GetActivePlaylist()
\r
440 #get the selected item from the tree
\r
441 selected_item = self._c['playlist']._c['tree'].GetSelection()
\r
442 #test if a playlist or a curve was double-clicked
\r
443 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
\r
447 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
448 while selected_item.IsOk():
\r
450 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
453 def _GetPlaylistTab(self, name):
\r
454 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
455 if page.caption == name:
\r
459 def select_plugin(self, _class=None, method=None, plugin=None):
\r
462 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
464 playlist = lib.playlist.Playlist(self, self.drivers)
\r
466 playlist.add_curve(item)
\r
467 if playlist.count > 0:
\r
468 playlist.name = self._GetUniquePlaylistName(name)
\r
470 self.AddTayliss(playlist)
\r
472 def AppliesPlotmanipulator(self, name):
\r
474 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
475 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
477 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
479 def ApplyPlotmanipulators(self, plot, plot_file):
\r
481 Apply all active plotmanipulators.
\r
483 if plot is not None and plot_file is not None:
\r
484 manipulated_plot = copy.deepcopy(plot)
\r
485 for plotmanipulator in self.plotmanipulators:
\r
486 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
487 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
488 return manipulated_plot
\r
490 def GetActiveFigure(self):
\r
491 playlist_name = self.GetActivePlaylistName()
\r
492 figure = self.playlists[playlist_name].figure
\r
493 if figure is not None:
\r
497 def GetActiveFile(self):
\r
498 playlist = self.GetActivePlaylist()
\r
499 if playlist is not None:
\r
500 return playlist.get_active_file()
\r
503 def GetActivePlot(self):
\r
504 playlist = self.GetActivePlaylist()
\r
505 if playlist is not None:
\r
506 return playlist.get_active_file().plot
\r
509 def GetDisplayedPlot(self):
\r
510 plot = copy.deepcopy(self.displayed_plot)
\r
512 #plot.curves = copy.deepcopy(plot.curves)
\r
515 def GetDisplayedPlotCorrected(self):
\r
516 plot = copy.deepcopy(self.displayed_plot)
\r
518 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
521 def GetDisplayedPlotRaw(self):
\r
522 plot = copy.deepcopy(self.displayed_plot)
\r
524 plot.curves = copy.deepcopy(plot.raw_curves)
\r
527 def GetDockArt(self):
\r
528 return self._c['manager'].GetArtProvider()
\r
530 def GetPlotmanipulator(self, name):
\r
532 Returns a plot manipulator function from its name
\r
534 for plotmanipulator in self.plotmanipulators:
\r
535 if plotmanipulator.name == name:
\r
536 return plotmanipulator
\r
539 def HasPlotmanipulator(self, name):
\r
541 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
543 for plotmanipulator in self.plotmanipulators:
\r
544 if plotmanipulator.command == name:
\r
549 def _on_dir_ctrl_left_double_click(self, event):
\r
550 file_path = self.panelFolders.GetPath()
\r
551 if os.path.isfile(file_path):
\r
552 if file_path.endswith('.hkp'):
\r
553 self.do_loadlist(file_path)
\r
556 def _on_erase_background(self, event):
\r
559 def _on_notebook_page_close(self, event):
\r
560 ctrl = event.GetEventObject()
\r
561 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
562 self.DeleteFromPlaylists(playlist_name)
\r
564 def OnPaneClose(self, event):
\r
567 def OnPropGridChanged (self, event):
\r
568 prop = event.GetProperty()
\r
570 item_section = self.panelProperties.SelectedTreeItem
\r
571 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
572 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
573 config = self.gui.config[plugin]
\r
574 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
575 property_key = prop.GetName()
\r
576 property_value = prop.GetDisplayedString()
\r
578 config[property_section][property_key]['value'] = property_value
\r
580 def OnResultsCheck(self, index, flag):
\r
581 results = self.GetActivePlot().results
\r
582 if results.has_key(self.results_str):
\r
583 results[self.results_str].results[index].visible = flag
\r
584 results[self.results_str].update()
\r
588 def _on_size(self, event):
\r
591 def UpdatePlaylistsTreeSelection(self):
\r
592 playlist = self.GetActivePlaylist()
\r
593 if playlist is not None:
\r
594 if playlist.index >= 0:
\r
595 self._c['status bar'].set_playlist(playlist)
\r
599 def _on_curve_select(self, playlist, curve):
\r
600 #create the plot tab and add playlist to the dictionary
\r
601 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
602 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
603 #tab_index = self._c['notebook'].GetSelection()
\r
604 playlist.figure = plotPanel.get_figure()
\r
605 self.playlists[playlist.name] = playlist
\r
606 #self.playlists[playlist.name] = [playlist, figure]
\r
607 self._c['status bar'].set_playlist(playlist)
\r
612 def _on_playlist_left_doubleclick(self):
\r
613 index = self._c['notebook'].GetSelection()
\r
614 current_playlist = self._c['notebook'].GetPageText(index)
\r
615 if current_playlist != playlist_name:
\r
616 index = self._GetPlaylistTab(playlist_name)
\r
617 self._c['notebook'].SetSelection(index)
\r
618 self._c['status bar'].set_playlist(playlist)
\r
622 def _on_playlist_delete(self, playlist):
\r
623 notebook = self.Parent.plotNotebook
\r
624 index = self.Parent._GetPlaylistTab(playlist.name)
\r
625 notebook.SetSelection(index)
\r
626 notebook.DeletePage(notebook.GetSelection())
\r
627 self.Parent.DeleteFromPlaylists(playlist_name)
\r
631 # Command panel interface
\r
633 def select_command(self, _class, method, command):
\r
634 #self.select_plugin(plugin=command.plugin)
\r
635 if 'assistant' in self._c:
\r
636 self._c['assitant'].ChangeValue(command.help)
\r
637 self._c['property editor'].clear()
\r
638 for argument in command.arguments:
\r
639 if argument.name == 'help':
\r
642 results = self.execute_command(
\r
643 command=self._command_by_name('playlists'))
\r
644 if not isinstance(results[-1], Success):
\r
645 self._postprocess_text(command, results=results)
\r
648 playlists = results[0]
\r
650 results = self.execute_command(
\r
651 command=self._command_by_name('playlist curves'))
\r
652 if not isinstance(results[-1], Success):
\r
653 self._postprocess_text(command, results=results)
\r
656 curves = results[0]
\r
658 p = prop_from_argument(
\r
659 argument, curves=curves, playlists=playlists)
\r
661 continue # property intentionally not handled (yet)
\r
662 self._c['property editor'].append_property(p)
\r
664 self.gui.config['selected command'] = command # TODO: push to engine
\r
668 # Note panel interface
\r
670 def _on_update_note(self, _class, method, text):
\r
671 """Sets the note for the active curve.
\r
673 # TODO: note list interface in NotePanel.
\r
674 self.execute_command(
\r
675 command=self._command_by_name('set note'),
\r
676 args={'note':text})
\r
680 # Playlist panel interface
\r
682 def _on_user_delete_playlist(self, _class, method, playlist):
\r
685 def _on_delete_playlist(self, _class, method, playlist):
\r
686 if hasattr(playlist, 'path') and playlist.path != None:
\r
687 os.remove(playlist.path)
\r
689 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
692 def _on_delete_curve(self, _class, method, playlist, curve):
\r
693 os.remove(curve.path)
\r
695 def _on_set_selected_playlist(self, _class, method, playlist):
\r
696 """TODO: playlists plugin with `jump to playlist`.
\r
700 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
701 """Call the `jump to curve` command.
\r
703 TODO: playlists plugin.
\r
705 # TODO: jump to playlist, get playlist
\r
706 index = playlist.index(curve)
\r
707 results = self.execute_command(
\r
708 command=self._command_by_name('jump to curve'),
\r
709 args={'index':index})
\r
710 if not isinstance(results[-1], Success):
\r
712 #results = self.execute_command(
\r
713 # command=self._command_by_name('get playlist'))
\r
714 #if not isinstance(results[-1], Success):
\r
716 self.execute_command(
\r
717 command=self._command_by_name('get curve'))
\r
723 def _next_curve(self, *args):
\r
724 """Call the `next curve` command.
\r
726 results = self.execute_command(
\r
727 command=self._command_by_name('next curve'))
\r
728 if isinstance(results[-1], Success):
\r
729 self.execute_command(
\r
730 command=self._command_by_name('get curve'))
\r
732 def _previous_curve(self, *args):
\r
733 """Call the `previous curve` command.
\r
735 results = self.execute_command(
\r
736 command=self._command_by_name('previous curve'))
\r
737 if isinstance(results[-1], Success):
\r
738 self.execute_command(
\r
739 command=self._command_by_name('get curve'))
\r
743 # Panel display handling
\r
745 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
746 pane = self._c['manager'].GetPane(panel_name)
\r
748 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
749 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
750 #folders_size = pane.GetSize()
\r
751 self.panelFolders.Fit()
\r
752 self._c['manager'].Update()
\r
754 def _setup_perspectives(self):
\r
755 """Add perspectives to menubar and _perspectives.
\r
757 self._perspectives = {
\r
758 'Default': self._c['manager'].SavePerspective(),
\r
760 path = self.gui.config['perspective path']
\r
761 if os.path.isdir(path):
\r
762 files = sorted(os.listdir(path))
\r
763 for fname in files:
\r
764 name, extension = os.path.splitext(fname)
\r
765 if extension != self.gui.config['perspective extension']:
\r
767 fpath = os.path.join(path, fname)
\r
768 if not os.path.isfile(fpath):
\r
771 with open(fpath, 'rU') as f:
\r
772 perspective = f.readline()
\r
774 self._perspectives[name] = perspective
\r
776 selected_perspective = self.gui.config['active perspective']
\r
777 if not self._perspectives.has_key(selected_perspective):
\r
778 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
780 self._restore_perspective(selected_perspective, force=True)
\r
781 self._update_perspective_menu()
\r
783 def _update_perspective_menu(self):
\r
784 self._c['menu bar']._c['perspective'].update(
\r
785 sorted(self._perspectives.keys()),
\r
786 self.gui.config['active perspective'])
\r
788 def _save_perspective(self, perspective, perspective_dir, name,
\r
790 path = os.path.join(perspective_dir, name)
\r
791 if extension != None:
\r
793 if not os.path.isdir(perspective_dir):
\r
794 os.makedirs(perspective_dir)
\r
795 with open(path, 'w') as f:
\r
796 f.write(perspective)
\r
797 self._perspectives[name] = perspective
\r
798 self._restore_perspective(name)
\r
799 self._update_perspective_menu()
\r
801 def _delete_perspectives(self, perspective_dir, names,
\r
803 self.log.debug('remove perspectives %s from %s'
\r
804 % (names, perspective_dir))
\r
806 path = os.path.join(perspective_dir, name)
\r
807 if extension != None:
\r
810 del(self._perspectives[name])
\r
811 self._update_perspective_menu()
\r
812 if self.gui.config['active perspective'] in names:
\r
813 self._restore_perspective('Default')
\r
814 # TODO: does this bug still apply?
\r
815 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
816 # http://trac.wxwidgets.org/ticket/3258
\r
817 # ) that makes the radio item indicator in the menu disappear.
\r
818 # The code should be fine once this issue is fixed.
\r
820 def _restore_perspective(self, name, force=False):
\r
821 if name != self.gui.config['active perspective'] or force == True:
\r
822 self.log.debug('restore perspective %s' % name)
\r
823 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
824 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
825 self._c['manager'].Update()
\r
826 for pane in self._c['manager'].GetAllPanes():
\r
827 view = self._c['menu bar']._c['view']
\r
828 if pane.name in view._c.keys():
\r
829 view._c[pane.name].Check(pane.window.IsShown())
\r
831 def _on_save_perspective(self, *args):
\r
832 perspective = self._c['manager'].SavePerspective()
\r
833 name = self.gui.config['active perspective']
\r
834 if name == 'Default':
\r
835 name = 'New perspective'
\r
836 name = select_save_file(
\r
837 directory=self.gui.config['perspective path'],
\r
839 extension=self.gui.config['perspective extension'],
\r
841 message='Enter a name for the new perspective:',
\r
842 caption='Save perspective')
\r
845 self._save_perspective(
\r
846 perspective, self.gui.config['perspective path'], name=name,
\r
847 extension=self.gui.config['perspective extension'])
\r
849 def _on_delete_perspective(self, *args, **kwargs):
\r
850 options = sorted([p for p in self._perspectives.keys()
\r
851 if p != 'Default'])
\r
852 dialog = SelectionDialog(
\r
854 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
855 button_id=wx.ID_DELETE,
\r
856 selection_style='multiple',
\r
858 title='Delete perspective(s)',
\r
859 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
860 dialog.CenterOnScreen()
\r
862 names = [options[i] for i in dialog.selected]
\r
864 self._delete_perspectives(
\r
865 self.gui.config['perspective path'], names=names,
\r
866 extension=self.gui.config['perspective extension'])
\r
868 def _on_select_perspective(self, _class, method, name):
\r
869 self._restore_perspective(name)
\r
873 class HookeApp (wx.App):
\r
874 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
876 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
879 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
881 self.commands = commands
\r
882 self.inqueue = inqueue
\r
883 self.outqueue = outqueue
\r
884 super(HookeApp, self).__init__(*args, **kwargs)
\r
887 self.SetAppName('Hooke')
\r
888 self.SetVendorName('')
\r
889 self._setup_splash_screen()
\r
891 height = int(self.gui.config['main height']) # HACK: config should convert
\r
892 width = int(self.gui.config['main width'])
\r
893 top = int(self.gui.config['main top'])
\r
894 left = int(self.gui.config['main left'])
\r
896 # Sometimes, the ini file gets confused and sets 'left' and
\r
897 # 'top' to large negative numbers. Here we catch and fix
\r
898 # this. Keep small negative numbers, the user might want
\r
906 'frame': HookeFrame(
\r
907 self.gui, self.commands, self.inqueue, self.outqueue,
\r
908 parent=None, title='Hooke',
\r
909 pos=(left, top), size=(width, height),
\r
910 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
912 self._c['frame'].Show(True)
\r
913 self.SetTopWindow(self._c['frame'])
\r
916 def _setup_splash_screen(self):
\r
917 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
918 path = self.gui.config['splash screen image']
\r
919 if os.path.isfile(path):
\r
920 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
922 bitmap=wx.Image(path).ConvertToBitmap(),
\r
923 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
924 milliseconds=duration,
\r
927 # For some reason splashDuration and sleep do not
\r
928 # correspond to each other at least not on Windows.
\r
929 # Maybe it's because duration is in milliseconds and
\r
930 # sleep in seconds. Thus we need to increase the
\r
931 # sleep time a bit. A factor of 1.2 seems to work.
\r
933 time.sleep(sleepFactor * duration / 1000)
\r
936 class GUI (UserInterface):
\r
937 """wxWindows graphical user interface.
\r
939 def __init__(self):
\r
940 super(GUI, self).__init__(name='gui')
\r
942 def default_settings(self):
\r
943 """Return a list of :class:`hooke.config.Setting`\s for any
\r
944 configurable UI settings.
\r
946 The suggested section setting is::
\r
948 Setting(section=self.setting_section, help=self.__doc__)
\r
951 Setting(section=self.setting_section, help=self.__doc__),
\r
952 Setting(section=self.setting_section, option='icon image',
\r
953 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
954 help='Path to the hooke icon image.'),
\r
955 Setting(section=self.setting_section, option='show splash screen',
\r
957 help='Enable/disable the splash screen'),
\r
958 Setting(section=self.setting_section, option='splash screen image',
\r
959 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
960 help='Path to the Hooke splash screen image.'),
\r
961 Setting(section=self.setting_section, option='splash screen duration',
\r
963 help='Duration of the splash screen in milliseconds.'),
\r
964 Setting(section=self.setting_section, option='perspective path',
\r
965 value=os.path.join('resources', 'gui', 'perspective'),
\r
966 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
967 Setting(section=self.setting_section, option='perspective extension',
\r
969 help='Extension for perspective files.'),
\r
970 Setting(section=self.setting_section, option='hide extensions',
\r
972 help='Hide file extensions when displaying names.'),
\r
973 Setting(section=self.setting_section, option='plot legend',
\r
975 help='Enable/disable the plot legend.'),
\r
976 Setting(section=self.setting_section, option='plot SI format',
\r
978 help='Enable/disable SI plot axes numbering.'),
\r
979 Setting(section=self.setting_section, option='plot decimals',
\r
981 help='Number of decimal places to show if "plot SI format" is enabled.'),
\r
982 Setting(section=self.setting_section, option='folders-workdir',
\r
984 help='This should probably go...'),
\r
985 Setting(section=self.setting_section, option='folders-filters',
\r
987 help='This should probably go...'),
\r
988 Setting(section=self.setting_section, option='active perspective',
\r
990 help='Name of active perspective file (or "Default").'),
\r
991 Setting(section=self.setting_section, option='folders-filter-index',
\r
993 help='This should probably go...'),
\r
994 Setting(section=self.setting_section, option='main height',
\r
996 help='Height of main window in pixels.'),
\r
997 Setting(section=self.setting_section, option='main width',
\r
999 help='Width of main window in pixels.'),
\r
1000 Setting(section=self.setting_section, option='main top',
\r
1002 help='Pixels from screen top to top of main window.'),
\r
1003 Setting(section=self.setting_section, option='main left',
\r
1005 help='Pixels from screen left to left of main window.'),
\r
1006 Setting(section=self.setting_section, option='selected command',
\r
1007 value='load playlist',
\r
1008 help='Name of the initially selected command.'),
\r
1011 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1015 app = HookeApp(gui=self,
\r
1016 commands=commands,
\r
1017 inqueue=ui_to_command_queue,
\r
1018 outqueue=command_to_ui_queue,
\r
1019 redirect=redirect)
\r
1022 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1023 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r