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
425 def _GetActiveFileIndex(self):
\r
426 lib.playlist.Playlist = self.GetActivePlaylist()
\r
427 #get the selected item from the tree
\r
428 selected_item = self._c['playlist']._c['tree'].GetSelection()
\r
429 #test if a playlist or a curve was double-clicked
\r
430 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
\r
434 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
435 while selected_item.IsOk():
\r
437 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
440 def _GetPlaylistTab(self, name):
\r
441 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
442 if page.caption == name:
\r
446 def select_plugin(self, _class=None, method=None, plugin=None):
\r
449 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
451 playlist = lib.playlist.Playlist(self, self.drivers)
\r
453 playlist.add_curve(item)
\r
454 if playlist.count > 0:
\r
455 playlist.name = self._GetUniquePlaylistName(name)
\r
457 self.AddTayliss(playlist)
\r
459 def AppliesPlotmanipulator(self, name):
\r
461 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
462 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
464 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
466 def ApplyPlotmanipulators(self, plot, plot_file):
\r
468 Apply all active plotmanipulators.
\r
470 if plot is not None and plot_file is not None:
\r
471 manipulated_plot = copy.deepcopy(plot)
\r
472 for plotmanipulator in self.plotmanipulators:
\r
473 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
474 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
475 return manipulated_plot
\r
477 def GetActiveFigure(self):
\r
478 playlist_name = self.GetActivePlaylistName()
\r
479 figure = self.playlists[playlist_name].figure
\r
480 if figure is not None:
\r
484 def GetActiveFile(self):
\r
485 playlist = self.GetActivePlaylist()
\r
486 if playlist is not None:
\r
487 return playlist.get_active_file()
\r
490 def GetActivePlot(self):
\r
491 playlist = self.GetActivePlaylist()
\r
492 if playlist is not None:
\r
493 return playlist.get_active_file().plot
\r
496 def GetDisplayedPlot(self):
\r
497 plot = copy.deepcopy(self.displayed_plot)
\r
499 #plot.curves = copy.deepcopy(plot.curves)
\r
502 def GetDisplayedPlotCorrected(self):
\r
503 plot = copy.deepcopy(self.displayed_plot)
\r
505 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
508 def GetDisplayedPlotRaw(self):
\r
509 plot = copy.deepcopy(self.displayed_plot)
\r
511 plot.curves = copy.deepcopy(plot.raw_curves)
\r
514 def GetDockArt(self):
\r
515 return self._c['manager'].GetArtProvider()
\r
517 def GetPlotmanipulator(self, name):
\r
519 Returns a plot manipulator function from its name
\r
521 for plotmanipulator in self.plotmanipulators:
\r
522 if plotmanipulator.name == name:
\r
523 return plotmanipulator
\r
526 def HasPlotmanipulator(self, name):
\r
528 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
530 for plotmanipulator in self.plotmanipulators:
\r
531 if plotmanipulator.command == name:
\r
536 def _on_dir_ctrl_left_double_click(self, event):
\r
537 file_path = self.panelFolders.GetPath()
\r
538 if os.path.isfile(file_path):
\r
539 if file_path.endswith('.hkp'):
\r
540 self.do_loadlist(file_path)
\r
543 def _on_erase_background(self, event):
\r
546 def _on_notebook_page_close(self, event):
\r
547 ctrl = event.GetEventObject()
\r
548 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
549 self.DeleteFromPlaylists(playlist_name)
\r
551 def OnPaneClose(self, event):
\r
554 def OnPropGridChanged (self, event):
\r
555 prop = event.GetProperty()
\r
557 item_section = self.panelProperties.SelectedTreeItem
\r
558 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
559 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
560 config = self.gui.config[plugin]
\r
561 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
562 property_key = prop.GetName()
\r
563 property_value = prop.GetDisplayedString()
\r
565 config[property_section][property_key]['value'] = property_value
\r
567 def OnResultsCheck(self, index, flag):
\r
568 results = self.GetActivePlot().results
\r
569 if results.has_key(self.results_str):
\r
570 results[self.results_str].results[index].visible = flag
\r
571 results[self.results_str].update()
\r
575 def _on_size(self, event):
\r
578 def OnUpdateNote(self, event):
\r
580 Saves the note to the active file.
\r
582 active_file = self.GetActiveFile()
\r
583 active_file.note = self.panelNote.Editor.GetValue()
\r
585 def UpdateNote(self):
\r
586 #update the note for the active file
\r
587 active_file = self.GetActiveFile()
\r
588 if active_file is not None:
\r
589 self.panelNote.Editor.SetValue(active_file.note)
\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 # Playlist panel interface
\r
670 def _on_user_delete_playlist(self, _class, method, playlist):
\r
673 def _on_delete_playlist(self, _class, method, playlist):
\r
674 if hasattr(playlist, 'path') and playlist.path != None:
\r
675 os.remove(playlist.path)
\r
677 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
680 def _on_delete_curve(self, _class, method, playlist, curve):
\r
681 os.remove(curve.path)
\r
683 def _on_set_selected_playlist(self, _class, method, playlist):
\r
684 """TODO: playlists plugin with `jump to playlist`.
\r
688 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
689 """Call the `jump to curve` command.
\r
691 TODO: playlists plugin.
\r
693 # TODO: jump to playlist, get playlist
\r
694 index = playlist.index(curve)
\r
695 results = self.execute_command(
\r
696 command=self._command_by_name('jump to curve'),
\r
697 args={'index':index})
\r
698 if not isinstance(results[-1], Success):
\r
700 #results = self.execute_command(
\r
701 # command=self._command_by_name('get playlist'))
\r
702 #if not isinstance(results[-1], Success):
\r
704 self.execute_command(
\r
705 command=self._command_by_name('get curve'))
\r
711 def _next_curve(self, *args):
\r
712 """Call the `next curve` command.
\r
714 results = self.execute_command(
\r
715 command=self._command_by_name('next curve'))
\r
716 if isinstance(results[-1], Success):
\r
717 self.execute_command(
\r
718 command=self._command_by_name('get curve'))
\r
720 def _previous_curve(self, *args):
\r
721 """Call the `previous curve` command.
\r
723 results = self.execute_command(
\r
724 command=self._command_by_name('previous curve'))
\r
725 if isinstance(results[-1], Success):
\r
726 self.execute_command(
\r
727 command=self._command_by_name('get curve'))
\r
731 # Panel display handling
\r
733 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
734 pane = self._c['manager'].GetPane(panel_name)
\r
736 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
737 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
738 #folders_size = pane.GetSize()
\r
739 self.panelFolders.Fit()
\r
740 self._c['manager'].Update()
\r
742 def _setup_perspectives(self):
\r
743 """Add perspectives to menubar and _perspectives.
\r
745 self._perspectives = {
\r
746 'Default': self._c['manager'].SavePerspective(),
\r
748 path = self.gui.config['perspective path']
\r
749 if os.path.isdir(path):
\r
750 files = sorted(os.listdir(path))
\r
751 for fname in files:
\r
752 name, extension = os.path.splitext(fname)
\r
753 if extension != self.gui.config['perspective extension']:
\r
755 fpath = os.path.join(path, fname)
\r
756 if not os.path.isfile(fpath):
\r
759 with open(fpath, 'rU') as f:
\r
760 perspective = f.readline()
\r
762 self._perspectives[name] = perspective
\r
764 selected_perspective = self.gui.config['active perspective']
\r
765 if not self._perspectives.has_key(selected_perspective):
\r
766 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
768 self._restore_perspective(selected_perspective, force=True)
\r
769 self._update_perspective_menu()
\r
771 def _update_perspective_menu(self):
\r
772 self._c['menu bar']._c['perspective'].update(
\r
773 sorted(self._perspectives.keys()),
\r
774 self.gui.config['active perspective'])
\r
776 def _save_perspective(self, perspective, perspective_dir, name,
\r
778 path = os.path.join(perspective_dir, name)
\r
779 if extension != None:
\r
781 if not os.path.isdir(perspective_dir):
\r
782 os.makedirs(perspective_dir)
\r
783 with open(path, 'w') as f:
\r
784 f.write(perspective)
\r
785 self._perspectives[name] = perspective
\r
786 self._restore_perspective(name)
\r
787 self._update_perspective_menu()
\r
789 def _delete_perspectives(self, perspective_dir, names,
\r
791 self.log.debug('remove perspectives %s from %s'
\r
792 % (names, perspective_dir))
\r
794 path = os.path.join(perspective_dir, name)
\r
795 if extension != None:
\r
798 del(self._perspectives[name])
\r
799 self._update_perspective_menu()
\r
800 if self.gui.config['active perspective'] in names:
\r
801 self._restore_perspective('Default')
\r
802 # TODO: does this bug still apply?
\r
803 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
804 # http://trac.wxwidgets.org/ticket/3258
\r
805 # ) that makes the radio item indicator in the menu disappear.
\r
806 # The code should be fine once this issue is fixed.
\r
808 def _restore_perspective(self, name, force=False):
\r
809 if name != self.gui.config['active perspective'] or force == True:
\r
810 self.log.debug('restore perspective %s' % name)
\r
811 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
812 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
813 self._c['manager'].Update()
\r
814 for pane in self._c['manager'].GetAllPanes():
\r
815 view = self._c['menu bar']._c['view']
\r
816 if pane.name in view._c.keys():
\r
817 view._c[pane.name].Check(pane.window.IsShown())
\r
819 def _on_save_perspective(self, *args):
\r
820 perspective = self._c['manager'].SavePerspective()
\r
821 name = self.gui.config['active perspective']
\r
822 if name == 'Default':
\r
823 name = 'New perspective'
\r
824 name = select_save_file(
\r
825 directory=self.gui.config['perspective path'],
\r
827 extension=self.gui.config['perspective extension'],
\r
829 message='Enter a name for the new perspective:',
\r
830 caption='Save perspective')
\r
833 self._save_perspective(
\r
834 perspective, self.gui.config['perspective path'], name=name,
\r
835 extension=self.gui.config['perspective extension'])
\r
837 def _on_delete_perspective(self, *args, **kwargs):
\r
838 options = sorted([p for p in self._perspectives.keys()
\r
839 if p != 'Default'])
\r
840 dialog = SelectionDialog(
\r
842 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
843 button_id=wx.ID_DELETE,
\r
844 selection_style='multiple',
\r
846 title='Delete perspective(s)',
\r
847 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
848 dialog.CenterOnScreen()
\r
850 names = [options[i] for i in dialog.selected]
\r
852 self._delete_perspectives(
\r
853 self.gui.config['perspective path'], names=names,
\r
854 extension=self.gui.config['perspective extension'])
\r
856 def _on_select_perspective(self, _class, method, name):
\r
857 self._restore_perspective(name)
\r
861 class HookeApp (wx.App):
\r
862 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
864 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
867 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
869 self.commands = commands
\r
870 self.inqueue = inqueue
\r
871 self.outqueue = outqueue
\r
872 super(HookeApp, self).__init__(*args, **kwargs)
\r
875 self.SetAppName('Hooke')
\r
876 self.SetVendorName('')
\r
877 self._setup_splash_screen()
\r
879 height = int(self.gui.config['main height']) # HACK: config should convert
\r
880 width = int(self.gui.config['main width'])
\r
881 top = int(self.gui.config['main top'])
\r
882 left = int(self.gui.config['main left'])
\r
884 # Sometimes, the ini file gets confused and sets 'left' and
\r
885 # 'top' to large negative numbers. Here we catch and fix
\r
886 # this. Keep small negative numbers, the user might want
\r
894 'frame': HookeFrame(
\r
895 self.gui, self.commands, self.inqueue, self.outqueue,
\r
896 parent=None, title='Hooke',
\r
897 pos=(left, top), size=(width, height),
\r
898 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
900 self._c['frame'].Show(True)
\r
901 self.SetTopWindow(self._c['frame'])
\r
904 def _setup_splash_screen(self):
\r
905 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
906 path = self.gui.config['splash screen image']
\r
907 if os.path.isfile(path):
\r
908 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
910 bitmap=wx.Image(path).ConvertToBitmap(),
\r
911 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
912 milliseconds=duration,
\r
915 # For some reason splashDuration and sleep do not
\r
916 # correspond to each other at least not on Windows.
\r
917 # Maybe it's because duration is in milliseconds and
\r
918 # sleep in seconds. Thus we need to increase the
\r
919 # sleep time a bit. A factor of 1.2 seems to work.
\r
921 time.sleep(sleepFactor * duration / 1000)
\r
924 class GUI (UserInterface):
\r
925 """wxWindows graphical user interface.
\r
927 def __init__(self):
\r
928 super(GUI, self).__init__(name='gui')
\r
930 def default_settings(self):
\r
931 """Return a list of :class:`hooke.config.Setting`\s for any
\r
932 configurable UI settings.
\r
934 The suggested section setting is::
\r
936 Setting(section=self.setting_section, help=self.__doc__)
\r
939 Setting(section=self.setting_section, help=self.__doc__),
\r
940 Setting(section=self.setting_section, option='icon image',
\r
941 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
942 help='Path to the hooke icon image.'),
\r
943 Setting(section=self.setting_section, option='show splash screen',
\r
945 help='Enable/disable the splash screen'),
\r
946 Setting(section=self.setting_section, option='splash screen image',
\r
947 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
948 help='Path to the Hooke splash screen image.'),
\r
949 Setting(section=self.setting_section, option='splash screen duration',
\r
951 help='Duration of the splash screen in milliseconds.'),
\r
952 Setting(section=self.setting_section, option='perspective path',
\r
953 value=os.path.join('resources', 'gui', 'perspective'),
\r
954 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
955 Setting(section=self.setting_section, option='perspective extension',
\r
957 help='Extension for perspective files.'),
\r
958 Setting(section=self.setting_section, option='hide extensions',
\r
960 help='Hide file extensions when displaying names.'),
\r
961 Setting(section=self.setting_section, option='plot legend',
\r
963 help='Enable/disable the plot legend.'),
\r
964 Setting(section=self.setting_section, option='plot SI format',
\r
966 help='Enable/disable SI plot axes numbering.'),
\r
967 Setting(section=self.setting_section, option='plot decimals',
\r
969 help='Number of decimal places to show if "plot SI format" is enabled.'),
\r
970 Setting(section=self.setting_section, option='folders-workdir',
\r
972 help='This should probably go...'),
\r
973 Setting(section=self.setting_section, option='folders-filters',
\r
975 help='This should probably go...'),
\r
976 Setting(section=self.setting_section, option='active perspective',
\r
978 help='Name of active perspective file (or "Default").'),
\r
979 Setting(section=self.setting_section, option='folders-filter-index',
\r
981 help='This should probably go...'),
\r
982 Setting(section=self.setting_section, option='main height',
\r
984 help='Height of main window in pixels.'),
\r
985 Setting(section=self.setting_section, option='main width',
\r
987 help='Width of main window in pixels.'),
\r
988 Setting(section=self.setting_section, option='main top',
\r
990 help='Pixels from screen top to top of main window.'),
\r
991 Setting(section=self.setting_section, option='main left',
\r
993 help='Pixels from screen left to left of main window.'),
\r
994 Setting(section=self.setting_section, option='selected command',
\r
995 value='load playlist',
\r
996 help='Name of the initially selected command.'),
\r
999 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1003 app = HookeApp(gui=self,
\r
1004 commands=commands,
\r
1005 inqueue=ui_to_command_queue,
\r
1006 outqueue=command_to_ui_queue,
\r
1007 redirect=redirect)
\r
1010 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1011 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r