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 # ('note', panel.note.Note(
\r
138 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
139 # size=(160, 200)), 'left'),
\r
140 # ('notebook', Notebook(
\r
142 # pos=wx.Point(client_size.x, client_size.y),
\r
143 # size=wx.Size(430, 200),
\r
144 # style=aui.AUI_NB_DEFAULT_STYLE
\r
145 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
146 (panel.PANELS['commands'](
\r
147 commands=self.commands,
\r
148 selected=self.gui.config['selected command'],
\r
150 'execute': self.execute_command,
\r
151 'select_plugin': self.select_plugin,
\r
152 'select_command': self.select_command,
\r
153 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
156 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
157 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
160 (panel.PANELS['propertyeditor'](
\r
163 style=wx.WANTS_CHARS,
\r
164 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
166 # ('assistant', wx.TextCtrl(
\r
168 # pos=wx.Point(0, 0),
\r
169 # size=wx.Size(150, 90),
\r
170 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
171 (panel.PANELS['plot'](
\r
175 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
176 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
179 (panel.PANELS['output'](
\r
181 pos=wx.Point(0, 0),
\r
182 size=wx.Size(150, 90),
\r
183 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
185 # ('results', panel.results.Results(self), 'bottom'),
\r
187 self._add_panel(p, style)
\r
188 #self._c['assistant'].SetEditable(False)
\r
190 def _add_panel(self, panel, style):
\r
191 self._c[panel.name] = panel
\r
192 m_name = panel.managed_name
\r
193 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
\r
194 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
197 elif style == 'center':
\r
199 elif style == 'left':
\r
201 elif style == 'right':
\r
204 assert style == 'bottom', style
\r
206 self._c['manager'].AddPane(panel, info)
\r
208 def _setup_toolbars(self):
\r
209 self._c['navigation bar'] = navbar.NavBar(
\r
211 'next': self._next_curve,
\r
212 'previous': self._previous_curve,
\r
215 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
216 self._c['manager'].AddPane(
\r
217 self._c['navigation bar'],
\r
218 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
219 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
220 ).RightDockable(False))
\r
222 def _bind_events(self):
\r
223 # TODO: figure out if we can use the eventManager for menu
\r
224 # ranges and events of 'self' without raising an assertion
\r
226 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
227 self.Bind(wx.EVT_SIZE, self._on_size)
\r
228 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
229 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
230 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
232 return # TODO: cleanup
\r
233 for value in self._c['menu bar']._c['view']._c.values():
\r
234 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
236 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
237 self._c['menu bar']._c['perspective']._c['save'])
\r
238 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
239 self._c['menu bar']._c['perspective']._c['delete'])
\r
241 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
242 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
244 # TODO: playlist callbacks
\r
245 return # TODO: cleanup
\r
246 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
248 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
250 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
252 def _on_about(self, *args):
\r
253 dialog = wx.MessageDialog(
\r
255 message=self.gui._splash_text(extra_info={
\r
256 'get-details':'click "Help -> License"'},
\r
258 caption='About Hooke',
\r
259 style=wx.OK|wx.ICON_INFORMATION)
\r
263 def _on_close(self, *args):
\r
264 self.log.info('closing GUI framework')
\r
266 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
267 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
268 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
269 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
270 # push changes back to Hooke.config?
\r
271 self._c['manager'].UnInit()
\r
272 del self._c['manager']
\r
277 # Panel utility functions
\r
279 def _file_name(self, name):
\r
280 """Cleanup names according to configured preferences.
\r
282 if self.gui.config['hide extensions'] == 'True': # HACK: config should decode
\r
283 name,ext = os.path.splitext(name)
\r
290 def _command_by_name(self, name):
\r
291 cs = [c for c in self.commands if c.name == name]
\r
293 raise KeyError(name)
\r
295 raise Exception('Multiple commands named "%s"' % name)
\r
298 def execute_command(self, _class=None, method=None,
\r
299 command=None, args=None):
\r
302 if ('property editor' in self._c
\r
303 and self.gui.config['selected command'] == command):
\r
304 arg_names = [arg.name for arg in command.arguments]
\r
305 for name,value in self._c['property editor'].get_values().items():
\r
306 if name in arg_names:
\r
308 self.log.debug('executing %s with %s' % (command.name, args))
\r
309 self.inqueue.put(CommandMessage(command, args))
\r
312 msg = self.outqueue.get()
\r
313 results.append(msg)
\r
314 if isinstance(msg, Exit):
\r
317 elif isinstance(msg, CommandExit):
\r
318 # TODO: display command complete
\r
320 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
321 self.gui.reload_config(msg.config)
\r
323 elif isinstance(msg, Request):
\r
324 h = handler.HANDLERS[msg.type]
\r
325 h.run(self, msg) # TODO: pause for response?
\r
328 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
329 self._postprocess_text)
\r
330 pp(command=command, args=args, results=results)
\r
333 def _handle_request(self, msg):
\r
334 """Repeatedly try to get a response to `msg`.
\r
337 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
338 prompt_string = prompt(msg)
\r
339 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
341 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
345 self.cmd.stdout.write(''.join([
\r
346 error.__class__.__name__, ': ', str(error), '\n']))
\r
347 self.cmd.stdout.write(prompt_string)
\r
348 value = parser(msg, self.cmd.stdin.readline())
\r
350 response = msg.response(value)
\r
352 except ValueError, error:
\r
354 self.inqueue.put(response)
\r
358 # Command-specific postprocessing
\r
360 def _postprocess_text(self, command, args={}, results=[]):
\r
361 """Print the string representation of the results to the Results window.
\r
363 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
364 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
365 doesn't print some internally handled messages
\r
366 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
368 for result in results:
\r
369 if isinstance(result, CommandExit):
\r
370 self._c['output'].write(result.__class__.__name__+'\n')
\r
371 self._c['output'].write(str(result).rstrip()+'\n')
\r
373 def _postprocess_load_playlist(self, command, args={}, results=None):
\r
374 """Update `self` to show the playlist.
\r
376 if not isinstance(results[-1], Success):
\r
377 self._postprocess_text(command, results=results)
\r
379 assert len(results) == 2, results
\r
380 playlist = results[0]
\r
381 self._c['playlist']._c['tree'].add_playlist(playlist)
\r
383 def _postprocess_get_playlist(self, command, args={}, results=[]):
\r
384 if not isinstance(results[-1], Success):
\r
385 self._postprocess_text(command, results=results)
\r
387 assert len(results) == 2, results
\r
388 playlist = results[0]
\r
389 self._c['playlist']._c['tree'].update_playlist(playlist)
\r
391 def _postprocess_get_curve(self, command, args={}, results=[]):
\r
392 """Update `self` to show the curve.
\r
394 if not isinstance(results[-1], Success):
\r
395 self._postprocess_text(command, results=results)
\r
397 assert len(results) == 2, results
\r
399 if args.get('curve', None) == None:
\r
400 # the command defaults to the current curve of the current playlist
\r
401 results = self.execute_command(
\r
402 command=self._command_by_name('get playlist'))
\r
403 playlist = results[0]
\r
405 raise NotImplementedError()
\r
406 if 'playlist' in self._c:
\r
407 self._c['playlist']._c['tree'].set_selected_curve(
\r
409 if 'plot' in self._c:
\r
410 self._c['plot'].set_curve(curve, config=self.gui.config)
\r
412 def _postprocess_next_curve(self, command, args={}, results=[]):
\r
413 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
417 def _postprocess_previous_curve(self, command, args={}, results=[]):
\r
418 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
422 def _postprocess_zero_block_surface_contact_point(
\r
423 self, command, args={}, results=[]):
\r
424 """Update the curve, since the available columns may have changed.
\r
426 if isinstance(results[-1], Success):
\r
427 self.execute_command(
\r
428 command=self._command_by_name('get curve'))
\r
430 def _postprocess_add_block_force_array(
\r
431 self, command, args={}, results=[]):
\r
432 """Update the curve, since the available columns may have changed.
\r
434 if isinstance(results[-1], Success):
\r
435 self.execute_command(
\r
436 command=self._command_by_name('get curve'))
\r
442 def _GetActiveFileIndex(self):
\r
443 lib.playlist.Playlist = self.GetActivePlaylist()
\r
444 #get the selected item from the tree
\r
445 selected_item = self._c['playlist']._c['tree'].GetSelection()
\r
446 #test if a playlist or a curve was double-clicked
\r
447 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
\r
451 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
452 while selected_item.IsOk():
\r
454 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
457 def _GetPlaylistTab(self, name):
\r
458 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
459 if page.caption == name:
\r
463 def select_plugin(self, _class=None, method=None, plugin=None):
\r
466 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
468 playlist = lib.playlist.Playlist(self, self.drivers)
\r
470 playlist.add_curve(item)
\r
471 if playlist.count > 0:
\r
472 playlist.name = self._GetUniquePlaylistName(name)
\r
474 self.AddTayliss(playlist)
\r
476 def AppliesPlotmanipulator(self, name):
\r
478 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
479 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
481 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
483 def ApplyPlotmanipulators(self, plot, plot_file):
\r
485 Apply all active plotmanipulators.
\r
487 if plot is not None and plot_file is not None:
\r
488 manipulated_plot = copy.deepcopy(plot)
\r
489 for plotmanipulator in self.plotmanipulators:
\r
490 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
491 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
492 return manipulated_plot
\r
494 def GetActiveFigure(self):
\r
495 playlist_name = self.GetActivePlaylistName()
\r
496 figure = self.playlists[playlist_name].figure
\r
497 if figure is not None:
\r
501 def GetActiveFile(self):
\r
502 playlist = self.GetActivePlaylist()
\r
503 if playlist is not None:
\r
504 return playlist.get_active_file()
\r
507 def GetActivePlot(self):
\r
508 playlist = self.GetActivePlaylist()
\r
509 if playlist is not None:
\r
510 return playlist.get_active_file().plot
\r
513 def GetDisplayedPlot(self):
\r
514 plot = copy.deepcopy(self.displayed_plot)
\r
516 #plot.curves = copy.deepcopy(plot.curves)
\r
519 def GetDisplayedPlotCorrected(self):
\r
520 plot = copy.deepcopy(self.displayed_plot)
\r
522 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
525 def GetDisplayedPlotRaw(self):
\r
526 plot = copy.deepcopy(self.displayed_plot)
\r
528 plot.curves = copy.deepcopy(plot.raw_curves)
\r
531 def GetDockArt(self):
\r
532 return self._c['manager'].GetArtProvider()
\r
534 def GetPlotmanipulator(self, name):
\r
536 Returns a plot manipulator function from its name
\r
538 for plotmanipulator in self.plotmanipulators:
\r
539 if plotmanipulator.name == name:
\r
540 return plotmanipulator
\r
543 def HasPlotmanipulator(self, name):
\r
545 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
547 for plotmanipulator in self.plotmanipulators:
\r
548 if plotmanipulator.command == name:
\r
553 def _on_dir_ctrl_left_double_click(self, event):
\r
554 file_path = self.panelFolders.GetPath()
\r
555 if os.path.isfile(file_path):
\r
556 if file_path.endswith('.hkp'):
\r
557 self.do_loadlist(file_path)
\r
560 def _on_erase_background(self, event):
\r
563 def _on_notebook_page_close(self, event):
\r
564 ctrl = event.GetEventObject()
\r
565 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
566 self.DeleteFromPlaylists(playlist_name)
\r
568 def OnPaneClose(self, event):
\r
571 def OnPropGridChanged (self, event):
\r
572 prop = event.GetProperty()
\r
574 item_section = self.panelProperties.SelectedTreeItem
\r
575 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
576 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
577 config = self.gui.config[plugin]
\r
578 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
579 property_key = prop.GetName()
\r
580 property_value = prop.GetDisplayedString()
\r
582 config[property_section][property_key]['value'] = property_value
\r
584 def OnResultsCheck(self, index, flag):
\r
585 results = self.GetActivePlot().results
\r
586 if results.has_key(self.results_str):
\r
587 results[self.results_str].results[index].visible = flag
\r
588 results[self.results_str].update()
\r
592 def _on_size(self, event):
\r
595 def OnUpdateNote(self, event):
\r
597 Saves the note to the active file.
\r
599 active_file = self.GetActiveFile()
\r
600 active_file.note = self.panelNote.Editor.GetValue()
\r
602 def UpdateNote(self):
\r
603 #update the note for the active file
\r
604 active_file = self.GetActiveFile()
\r
605 if active_file is not None:
\r
606 self.panelNote.Editor.SetValue(active_file.note)
\r
608 def UpdatePlaylistsTreeSelection(self):
\r
609 playlist = self.GetActivePlaylist()
\r
610 if playlist is not None:
\r
611 if playlist.index >= 0:
\r
612 self._c['status bar'].set_playlist(playlist)
\r
616 def _on_curve_select(self, playlist, curve):
\r
617 #create the plot tab and add playlist to the dictionary
\r
618 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
619 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
620 #tab_index = self._c['notebook'].GetSelection()
\r
621 playlist.figure = plotPanel.get_figure()
\r
622 self.playlists[playlist.name] = playlist
\r
623 #self.playlists[playlist.name] = [playlist, figure]
\r
624 self._c['status bar'].set_playlist(playlist)
\r
629 def _on_playlist_left_doubleclick(self):
\r
630 index = self._c['notebook'].GetSelection()
\r
631 current_playlist = self._c['notebook'].GetPageText(index)
\r
632 if current_playlist != playlist_name:
\r
633 index = self._GetPlaylistTab(playlist_name)
\r
634 self._c['notebook'].SetSelection(index)
\r
635 self._c['status bar'].set_playlist(playlist)
\r
639 def _on_playlist_delete(self, playlist):
\r
640 notebook = self.Parent.plotNotebook
\r
641 index = self.Parent._GetPlaylistTab(playlist.name)
\r
642 notebook.SetSelection(index)
\r
643 notebook.DeletePage(notebook.GetSelection())
\r
644 self.Parent.DeleteFromPlaylists(playlist_name)
\r
648 # Command panel interface
\r
650 def select_command(self, _class, method, command):
\r
651 #self.select_plugin(plugin=command.plugin)
\r
652 if 'assistant' in self._c:
\r
653 self._c['assitant'].ChangeValue(command.help)
\r
654 self._c['property editor'].clear()
\r
655 for argument in command.arguments:
\r
656 if argument.name == 'help':
\r
659 results = self.execute_command(
\r
660 command=self._command_by_name('playlists'))
\r
661 if not isinstance(results[-1], Success):
\r
662 self._postprocess_text(command, results=results)
\r
665 playlists = results[0]
\r
667 results = self.execute_command(
\r
668 command=self._command_by_name('playlist curves'))
\r
669 if not isinstance(results[-1], Success):
\r
670 self._postprocess_text(command, results=results)
\r
673 curves = results[0]
\r
675 p = prop_from_argument(
\r
676 argument, curves=curves, playlists=playlists)
\r
678 continue # property intentionally not handled (yet)
\r
679 self._c['property editor'].append_property(p)
\r
681 self.gui.config['selected command'] = command # TODO: push to engine
\r
685 # Playlist panel interface
\r
687 def _on_user_delete_playlist(self, _class, method, playlist):
\r
690 def _on_delete_playlist(self, _class, method, playlist):
\r
691 if hasattr(playlist, 'path') and playlist.path != None:
\r
692 os.remove(playlist.path)
\r
694 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
697 def _on_delete_curve(self, _class, method, playlist, curve):
\r
698 os.remove(curve.path)
\r
700 def _on_set_selected_playlist(self, _class, method, playlist):
\r
701 """TODO: playlists plugin with `jump to playlist`.
\r
705 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
706 """Call the `jump to curve` command.
\r
708 TODO: playlists plugin.
\r
710 # TODO: jump to playlist, get playlist
\r
711 index = playlist.index(curve)
\r
712 results = self.execute_command(
\r
713 command=self._command_by_name('jump to curve'),
\r
714 args={'index':index})
\r
715 if not isinstance(results[-1], Success):
\r
717 #results = self.execute_command(
\r
718 # command=self._command_by_name('get playlist'))
\r
719 #if not isinstance(results[-1], Success):
\r
721 self.execute_command(
\r
722 command=self._command_by_name('get curve'))
\r
728 def _next_curve(self, *args):
\r
729 """Call the `next curve` command.
\r
731 results = self.execute_command(
\r
732 command=self._command_by_name('next curve'))
\r
733 if isinstance(results[-1], Success):
\r
734 self.execute_command(
\r
735 command=self._command_by_name('get curve'))
\r
737 def _previous_curve(self, *args):
\r
738 """Call the `previous curve` command.
\r
740 results = self.execute_command(
\r
741 command=self._command_by_name('previous curve'))
\r
742 if isinstance(results[-1], Success):
\r
743 self.execute_command(
\r
744 command=self._command_by_name('get curve'))
\r
748 # Panel display handling
\r
750 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
751 pane = self._c['manager'].GetPane(panel_name)
\r
753 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
754 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
755 #folders_size = pane.GetSize()
\r
756 self.panelFolders.Fit()
\r
757 self._c['manager'].Update()
\r
759 def _setup_perspectives(self):
\r
760 """Add perspectives to menubar and _perspectives.
\r
762 self._perspectives = {
\r
763 'Default': self._c['manager'].SavePerspective(),
\r
765 path = self.gui.config['perspective path']
\r
766 if os.path.isdir(path):
\r
767 files = sorted(os.listdir(path))
\r
768 for fname in files:
\r
769 name, extension = os.path.splitext(fname)
\r
770 if extension != self.gui.config['perspective extension']:
\r
772 fpath = os.path.join(path, fname)
\r
773 if not os.path.isfile(fpath):
\r
776 with open(fpath, 'rU') as f:
\r
777 perspective = f.readline()
\r
779 self._perspectives[name] = perspective
\r
781 selected_perspective = self.gui.config['active perspective']
\r
782 if not self._perspectives.has_key(selected_perspective):
\r
783 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
785 self._restore_perspective(selected_perspective, force=True)
\r
786 self._update_perspective_menu()
\r
788 def _update_perspective_menu(self):
\r
789 self._c['menu bar']._c['perspective'].update(
\r
790 sorted(self._perspectives.keys()),
\r
791 self.gui.config['active perspective'])
\r
793 def _save_perspective(self, perspective, perspective_dir, name,
\r
795 path = os.path.join(perspective_dir, name)
\r
796 if extension != None:
\r
798 if not os.path.isdir(perspective_dir):
\r
799 os.makedirs(perspective_dir)
\r
800 with open(path, 'w') as f:
\r
801 f.write(perspective)
\r
802 self._perspectives[name] = perspective
\r
803 self._restore_perspective(name)
\r
804 self._update_perspective_menu()
\r
806 def _delete_perspectives(self, perspective_dir, names,
\r
808 self.log.debug('remove perspectives %s from %s'
\r
809 % (names, perspective_dir))
\r
811 path = os.path.join(perspective_dir, name)
\r
812 if extension != None:
\r
815 del(self._perspectives[name])
\r
816 self._update_perspective_menu()
\r
817 if self.gui.config['active perspective'] in names:
\r
818 self._restore_perspective('Default')
\r
819 # TODO: does this bug still apply?
\r
820 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
821 # http://trac.wxwidgets.org/ticket/3258
\r
822 # ) that makes the radio item indicator in the menu disappear.
\r
823 # The code should be fine once this issue is fixed.
\r
825 def _restore_perspective(self, name, force=False):
\r
826 if name != self.gui.config['active perspective'] or force == True:
\r
827 self.log.debug('restore perspective %s' % name)
\r
828 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
829 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
830 self._c['manager'].Update()
\r
831 for pane in self._c['manager'].GetAllPanes():
\r
832 view = self._c['menu bar']._c['view']
\r
833 if pane.name in view._c.keys():
\r
834 view._c[pane.name].Check(pane.window.IsShown())
\r
836 def _on_save_perspective(self, *args):
\r
837 perspective = self._c['manager'].SavePerspective()
\r
838 name = self.gui.config['active perspective']
\r
839 if name == 'Default':
\r
840 name = 'New perspective'
\r
841 name = select_save_file(
\r
842 directory=self.gui.config['perspective path'],
\r
844 extension=self.gui.config['perspective extension'],
\r
846 message='Enter a name for the new perspective:',
\r
847 caption='Save perspective')
\r
850 self._save_perspective(
\r
851 perspective, self.gui.config['perspective path'], name=name,
\r
852 extension=self.gui.config['perspective extension'])
\r
854 def _on_delete_perspective(self, *args, **kwargs):
\r
855 options = sorted([p for p in self._perspectives.keys()
\r
856 if p != 'Default'])
\r
857 dialog = SelectionDialog(
\r
859 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
860 button_id=wx.ID_DELETE,
\r
861 selection_style='multiple',
\r
863 title='Delete perspective(s)',
\r
864 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
865 dialog.CenterOnScreen()
\r
867 names = [options[i] for i in dialog.selected]
\r
869 self._delete_perspectives(
\r
870 self.gui.config['perspective path'], names=names,
\r
871 extension=self.gui.config['perspective extension'])
\r
873 def _on_select_perspective(self, _class, method, name):
\r
874 self._restore_perspective(name)
\r
878 class HookeApp (wx.App):
\r
879 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
881 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
884 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
886 self.commands = commands
\r
887 self.inqueue = inqueue
\r
888 self.outqueue = outqueue
\r
889 super(HookeApp, self).__init__(*args, **kwargs)
\r
892 self.SetAppName('Hooke')
\r
893 self.SetVendorName('')
\r
894 self._setup_splash_screen()
\r
896 height = int(self.gui.config['main height']) # HACK: config should convert
\r
897 width = int(self.gui.config['main width'])
\r
898 top = int(self.gui.config['main top'])
\r
899 left = int(self.gui.config['main left'])
\r
901 # Sometimes, the ini file gets confused and sets 'left' and
\r
902 # 'top' to large negative numbers. Here we catch and fix
\r
903 # this. Keep small negative numbers, the user might want
\r
911 'frame': HookeFrame(
\r
912 self.gui, self.commands, self.inqueue, self.outqueue,
\r
913 parent=None, title='Hooke',
\r
914 pos=(left, top), size=(width, height),
\r
915 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
917 self._c['frame'].Show(True)
\r
918 self.SetTopWindow(self._c['frame'])
\r
921 def _setup_splash_screen(self):
\r
922 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
923 path = self.gui.config['splash screen image']
\r
924 if os.path.isfile(path):
\r
925 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
927 bitmap=wx.Image(path).ConvertToBitmap(),
\r
928 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
929 milliseconds=duration,
\r
932 # For some reason splashDuration and sleep do not
\r
933 # correspond to each other at least not on Windows.
\r
934 # Maybe it's because duration is in milliseconds and
\r
935 # sleep in seconds. Thus we need to increase the
\r
936 # sleep time a bit. A factor of 1.2 seems to work.
\r
938 time.sleep(sleepFactor * duration / 1000)
\r
941 class GUI (UserInterface):
\r
942 """wxWindows graphical user interface.
\r
944 def __init__(self):
\r
945 super(GUI, self).__init__(name='gui')
\r
947 def default_settings(self):
\r
948 """Return a list of :class:`hooke.config.Setting`\s for any
\r
949 configurable UI settings.
\r
951 The suggested section setting is::
\r
953 Setting(section=self.setting_section, help=self.__doc__)
\r
956 Setting(section=self.setting_section, help=self.__doc__),
\r
957 Setting(section=self.setting_section, option='icon image',
\r
958 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
959 help='Path to the hooke icon image.'),
\r
960 Setting(section=self.setting_section, option='show splash screen',
\r
962 help='Enable/disable the splash screen'),
\r
963 Setting(section=self.setting_section, option='splash screen image',
\r
964 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
965 help='Path to the Hooke splash screen image.'),
\r
966 Setting(section=self.setting_section, option='splash screen duration',
\r
968 help='Duration of the splash screen in milliseconds.'),
\r
969 Setting(section=self.setting_section, option='perspective path',
\r
970 value=os.path.join('resources', 'gui', 'perspective'),
\r
971 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
972 Setting(section=self.setting_section, option='perspective extension',
\r
974 help='Extension for perspective files.'),
\r
975 Setting(section=self.setting_section, option='hide extensions',
\r
977 help='Hide file extensions when displaying names.'),
\r
978 Setting(section=self.setting_section, option='plot legend',
\r
980 help='Enable/disable the plot legend.'),
\r
981 Setting(section=self.setting_section, option='plot SI format',
\r
983 help='Enable/disable SI plot axes numbering.'),
\r
984 Setting(section=self.setting_section, option='plot decimals',
\r
986 help='Number of decimal places to show if "plot SI format" is enabled.'),
\r
987 Setting(section=self.setting_section, option='folders-workdir',
\r
989 help='This should probably go...'),
\r
990 Setting(section=self.setting_section, option='folders-filters',
\r
992 help='This should probably go...'),
\r
993 Setting(section=self.setting_section, option='active perspective',
\r
995 help='Name of active perspective file (or "Default").'),
\r
996 Setting(section=self.setting_section, option='folders-filter-index',
\r
998 help='This should probably go...'),
\r
999 Setting(section=self.setting_section, option='main height',
\r
1001 help='Height of main window in pixels.'),
\r
1002 Setting(section=self.setting_section, option='main width',
\r
1004 help='Width of main window in pixels.'),
\r
1005 Setting(section=self.setting_section, option='main top',
\r
1007 help='Pixels from screen top to top of main window.'),
\r
1008 Setting(section=self.setting_section, option='main left',
\r
1010 help='Pixels from screen left to left of main window.'),
\r
1011 Setting(section=self.setting_section, option='selected command',
\r
1012 value='load playlist',
\r
1013 help='Name of the initially selected command.'),
\r
1016 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1020 app = HookeApp(gui=self,
\r
1021 commands=commands,
\r
1022 inqueue=ui_to_command_queue,
\r
1023 outqueue=command_to_ui_queue,
\r
1024 redirect=redirect)
\r
1027 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1028 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r