3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
\r
10 wxversion.select(WX_GOOD)
\r
20 import wx.aui as aui
\r
21 import wx.lib.evtmgr as evtmgr
\r
22 # wxPropertyGrid is included in wxPython >= 2.9.1, see
\r
23 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
\r
24 # until then, we'll avoid it because of the *nix build problems.
\r
25 #import wx.propgrid as wxpg
\r
27 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
\r
28 from ...config import Setting
\r
29 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
30 from ...ui import UserInterface, CommandMessage
\r
31 from .dialog.selection import Selection as SelectionDialog
\r
32 from .dialog.save_file import select_save_file
\r
33 from . import menu as menu
\r
34 from . import navbar as navbar
\r
35 from . import panel as panel
\r
36 from .panel.propertyeditor import prop_from_argument, prop_from_setting
\r
37 from . import prettyformat as prettyformat
\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
47 self.commands = commands
\r
48 self.inqueue = inqueue
\r
49 self.outqueue = outqueue
\r
50 self._perspectives = {} # {name: perspective_str}
\r
53 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
55 # setup frame manager
\r
56 self._c['manager'] = aui.AuiManager()
\r
57 self._c['manager'].SetManagedWindow(self)
\r
59 # set the gradient and drag styles
\r
60 self._c['manager'].GetArtProvider().SetMetric(
\r
61 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
62 self._c['manager'].SetFlags(
\r
63 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
65 # Min size for the frame itself isn't completely done. See
\r
66 # the end of FrameManager::Update() for the test code. For
\r
67 # now, just hard code a frame minimum size.
\r
68 #self.SetMinSize(wx.Size(500, 500))
\r
70 self._setup_panels()
\r
71 self._setup_toolbars()
\r
72 self._c['manager'].Update() # commit pending changes
\r
74 # Create the menubar after the panes so that the default
\r
75 # perspective is created with all panes open
\r
76 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
\r
77 self._c['menu bar'] = menu.HookeMenuBar(
\r
81 'close': self._on_close,
\r
82 'about': self._on_about,
\r
83 'view_panel': self._on_panel_visibility,
\r
84 'save_perspective': self._on_save_perspective,
\r
85 'delete_perspective': self._on_delete_perspective,
\r
86 'select_perspective': self._on_select_perspective,
\r
88 self.SetMenuBar(self._c['menu bar'])
\r
90 self._c['status bar'] = statusbar.StatusBar(
\r
92 style=wx.ST_SIZEGRIP)
\r
93 self.SetStatusBar(self._c['status bar'])
\r
95 self._setup_perspectives()
\r
98 self.execute_command(
\r
99 command=self._command_by_name('load playlist'),
\r
100 args={'input':'test/data/test'},
\r
102 return # TODO: cleanup
\r
103 self.playlists = self._c['playlist'].Playlists
\r
104 self._displayed_plot = None
\r
105 #load default list, if possible
\r
106 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
\r
111 def _setup_panels(self):
\r
112 client_size = self.GetClientSize()
\r
114 # ('folders', wx.GenericDirCtrl(
\r
116 # dir=self.gui.config['folders-workdir'],
\r
118 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
119 # filter=self.gui.config['folders-filters'],
\r
120 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
121 (panel.PANELS['playlist'](
\r
123 'delete_playlist':self._on_user_delete_playlist,
\r
124 '_delete_playlist':self._on_delete_playlist,
\r
125 'delete_curve':self._on_user_delete_curve,
\r
126 '_delete_curve':self._on_delete_curve,
\r
127 '_on_set_selected_playlist':self._on_set_selected_playlist,
\r
128 '_on_set_selected_curve':self._on_set_selected_curve,
\r
131 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
132 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
135 # ('note', panel.note.Note(
\r
137 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
138 # size=(160, 200)), 'left'),
\r
139 # ('notebook', Notebook(
\r
141 # pos=wx.Point(client_size.x, client_size.y),
\r
142 # size=wx.Size(430, 200),
\r
143 # style=aui.AUI_NB_DEFAULT_STYLE
\r
144 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
145 (panel.PANELS['commands'](
\r
146 commands=self.commands,
\r
147 selected=self.gui.config['selected command'],
\r
149 'execute': self.execute_command,
\r
150 'select_plugin': self.select_plugin,
\r
151 'select_command': self.select_command,
\r
152 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
155 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
156 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
159 (panel.PANELS['propertyeditor'](
\r
162 style=wx.WANTS_CHARS,
\r
163 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
165 # ('assistant', wx.TextCtrl(
\r
167 # pos=wx.Point(0, 0),
\r
168 # size=wx.Size(150, 90),
\r
169 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
170 (panel.PANELS['plot'](
\r
174 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
175 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
178 (panel.PANELS['output'](
\r
180 pos=wx.Point(0, 0),
\r
181 size=wx.Size(150, 90),
\r
182 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
184 # ('results', panel.results.Results(self), 'bottom'),
\r
186 self._add_panel(p, style)
\r
187 #self._c['assistant'].SetEditable(False)
\r
189 def _add_panel(self, panel, style):
\r
190 self._c[panel.name] = panel
\r
191 m_name = panel.managed_name
\r
192 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
\r
193 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
196 elif style == 'center':
\r
198 elif style == 'left':
\r
200 elif style == 'right':
\r
203 assert style == 'bottom', style
\r
205 self._c['manager'].AddPane(panel, info)
\r
207 def _setup_toolbars(self):
\r
208 self._c['navigation bar'] = navbar.NavBar(
\r
210 'next': self._next_curve,
\r
211 'previous': self._previous_curve,
\r
214 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
215 self._c['manager'].AddPane(
\r
216 self._c['navigation bar'],
\r
217 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
218 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
219 ).RightDockable(False))
\r
221 def _bind_events(self):
\r
222 # TODO: figure out if we can use the eventManager for menu
\r
223 # ranges and events of 'self' without raising an assertion
\r
225 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
226 self.Bind(wx.EVT_SIZE, self._on_size)
\r
227 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
228 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
229 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
231 return # TODO: cleanup
\r
232 for value in self._c['menu bar']._c['view']._c.values():
\r
233 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
235 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
236 self._c['menu bar']._c['perspective']._c['save'])
\r
237 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
238 self._c['menu bar']._c['perspective']._c['delete'])
\r
240 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
241 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
243 # TODO: playlist callbacks
\r
244 return # TODO: cleanup
\r
245 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
247 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
249 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
251 def _on_about(self, *args):
\r
252 dialog = wx.MessageDialog(
\r
254 message=self.gui._splash_text(extra_info={
\r
255 'get-details':'click "Help -> License"'},
\r
257 caption='About Hooke',
\r
258 style=wx.OK|wx.ICON_INFORMATION)
\r
262 def _on_close(self, *args):
\r
264 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
265 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
266 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
267 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
268 # push changes back to Hooke.config?
\r
269 self._c['manager'].UnInit()
\r
270 del self._c['manager']
\r
275 # Panel utility functions
\r
277 def _file_name(self, name):
\r
278 """Cleanup names according to configured preferences.
\r
280 if self.gui.config['hide extensions'] == 'True': # HACK: config should decode
\r
281 name,ext = os.path.splitext(name)
\r
288 def _command_by_name(self, name):
\r
289 cs = [c for c in self.commands if c.name == name]
\r
291 raise KeyError(name)
\r
293 raise Exception('Multiple commands named "%s"' % name)
\r
296 def execute_command(self, _class=None, method=None,
\r
297 command=None, args=None):
\r
300 if ('property editor' in self._c
\r
301 and self.gui.config['selected command'] == command):
\r
302 arg_names = [arg.name for arg in command.arguments]
\r
303 for name,value in self._c['property editor'].get_values().items():
\r
304 if name in arg_names:
\r
306 print 'executing', command.name, args
\r
307 self.inqueue.put(CommandMessage(command, args))
\r
310 msg = self.outqueue.get()
\r
311 results.append(msg)
\r
312 if isinstance(msg, Exit):
\r
315 elif isinstance(msg, CommandExit):
\r
316 # TODO: display command complete
\r
318 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
319 self.gui.reload_config(msg.config)
\r
321 elif isinstance(msg, Request):
\r
322 h = handler.HANDLERS[msg.type]
\r
323 h.run(self, msg) # TODO: pause for response?
\r
326 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
327 self._postprocess_text)
\r
328 pp(command=command, args=args, results=results)
\r
331 def _handle_request(self, msg):
\r
332 """Repeatedly try to get a response to `msg`.
\r
335 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
336 prompt_string = prompt(msg)
\r
337 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
339 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
343 self.cmd.stdout.write(''.join([
\r
344 error.__class__.__name__, ': ', str(error), '\n']))
\r
345 self.cmd.stdout.write(prompt_string)
\r
346 value = parser(msg, self.cmd.stdin.readline())
\r
348 response = msg.response(value)
\r
350 except ValueError, error:
\r
352 self.inqueue.put(response)
\r
356 # Command-specific postprocessing
\r
358 def _postprocess_text(self, command, args={}, results=[]):
\r
359 """Print the string representation of the results to the Results window.
\r
361 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
362 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
363 doesn't print some internally handled messages
\r
364 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
366 for result in results:
\r
367 if isinstance(result, CommandExit):
\r
368 self._c['output'].write(result.__class__.__name__+'\n')
\r
369 self._c['output'].write(str(result).rstrip()+'\n')
\r
371 def _postprocess_load_playlist(self, command, args={}, results=None):
\r
372 """Update `self` to show the playlist.
\r
374 if not isinstance(results[-1], Success):
\r
375 self._postprocess_text(command, results=results)
\r
377 assert len(results) == 2, results
\r
378 playlist = results[0]
\r
379 self._c['playlist']._c['tree'].add_playlist(playlist)
\r
381 def _postprocess_get_playlist(self, command, args={}, results=[]):
\r
382 if not isinstance(results[-1], Success):
\r
383 self._postprocess_text(command, results=results)
\r
385 assert len(results) == 2, results
\r
386 playlist = results[0]
\r
387 self._c['playlist']._c['tree'].update_playlist(playlist)
\r
389 def _postprocess_get_curve(self, command, args={}, results=[]):
\r
390 """Update `self` to show the curve.
\r
392 if not isinstance(results[-1], Success):
\r
393 self._postprocess_text(command, results=results)
\r
395 assert len(results) == 2, results
\r
397 if args.get('curve', None) == None:
\r
398 # the command defaults to the current curve of the current playlist
\r
399 results = self.execute_command(
\r
400 command=self._command_by_name('get playlist'))
\r
401 playlist = results[0]
\r
403 raise NotImplementedError()
\r
404 if 'playlist' in self._c:
\r
405 self._c['playlist']._c['tree'].set_selected_curve(
\r
407 if 'plot' in self._c:
\r
408 self._c['plot'].set_curve(curve, config=self.gui.config)
\r
410 def _postprocess_next_curve(self, command, args={}, results=[]):
\r
411 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
415 def _postprocess_previous_curve(self, command, args={}, results=[]):
\r
416 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
423 def _GetActiveFileIndex(self):
\r
424 lib.playlist.Playlist = self.GetActivePlaylist()
\r
425 #get the selected item from the tree
\r
426 selected_item = self._c['playlist']._c['tree'].GetSelection()
\r
427 #test if a playlist or a curve was double-clicked
\r
428 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
\r
432 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
433 while selected_item.IsOk():
\r
435 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
438 def _GetPlaylistTab(self, name):
\r
439 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
440 if page.caption == name:
\r
444 def select_plugin(self, _class=None, method=None, plugin=None):
\r
447 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
449 playlist = lib.playlist.Playlist(self, self.drivers)
\r
451 playlist.add_curve(item)
\r
452 if playlist.count > 0:
\r
453 playlist.name = self._GetUniquePlaylistName(name)
\r
455 self.AddTayliss(playlist)
\r
457 def AppliesPlotmanipulator(self, name):
\r
459 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
460 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
462 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
464 def ApplyPlotmanipulators(self, plot, plot_file):
\r
466 Apply all active plotmanipulators.
\r
468 if plot is not None and plot_file is not None:
\r
469 manipulated_plot = copy.deepcopy(plot)
\r
470 for plotmanipulator in self.plotmanipulators:
\r
471 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
472 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
473 return manipulated_plot
\r
475 def GetActiveFigure(self):
\r
476 playlist_name = self.GetActivePlaylistName()
\r
477 figure = self.playlists[playlist_name].figure
\r
478 if figure is not None:
\r
482 def GetActiveFile(self):
\r
483 playlist = self.GetActivePlaylist()
\r
484 if playlist is not None:
\r
485 return playlist.get_active_file()
\r
488 def GetActivePlot(self):
\r
489 playlist = self.GetActivePlaylist()
\r
490 if playlist is not None:
\r
491 return playlist.get_active_file().plot
\r
494 def GetDisplayedPlot(self):
\r
495 plot = copy.deepcopy(self.displayed_plot)
\r
497 #plot.curves = copy.deepcopy(plot.curves)
\r
500 def GetDisplayedPlotCorrected(self):
\r
501 plot = copy.deepcopy(self.displayed_plot)
\r
503 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
506 def GetDisplayedPlotRaw(self):
\r
507 plot = copy.deepcopy(self.displayed_plot)
\r
509 plot.curves = copy.deepcopy(plot.raw_curves)
\r
512 def GetDockArt(self):
\r
513 return self._c['manager'].GetArtProvider()
\r
515 def GetPlotmanipulator(self, name):
\r
517 Returns a plot manipulator function from its name
\r
519 for plotmanipulator in self.plotmanipulators:
\r
520 if plotmanipulator.name == name:
\r
521 return plotmanipulator
\r
524 def HasPlotmanipulator(self, name):
\r
526 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
528 for plotmanipulator in self.plotmanipulators:
\r
529 if plotmanipulator.command == name:
\r
534 def _on_dir_ctrl_left_double_click(self, event):
\r
535 file_path = self.panelFolders.GetPath()
\r
536 if os.path.isfile(file_path):
\r
537 if file_path.endswith('.hkp'):
\r
538 self.do_loadlist(file_path)
\r
541 def _on_erase_background(self, event):
\r
544 def _on_notebook_page_close(self, event):
\r
545 ctrl = event.GetEventObject()
\r
546 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
547 self.DeleteFromPlaylists(playlist_name)
\r
549 def OnPaneClose(self, event):
\r
552 def OnPropGridChanged (self, event):
\r
553 prop = event.GetProperty()
\r
555 item_section = self.panelProperties.SelectedTreeItem
\r
556 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
557 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
558 config = self.gui.config[plugin]
\r
559 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
560 property_key = prop.GetName()
\r
561 property_value = prop.GetDisplayedString()
\r
563 config[property_section][property_key]['value'] = property_value
\r
565 def OnResultsCheck(self, index, flag):
\r
566 results = self.GetActivePlot().results
\r
567 if results.has_key(self.results_str):
\r
568 results[self.results_str].results[index].visible = flag
\r
569 results[self.results_str].update()
\r
573 def _on_size(self, event):
\r
576 def OnUpdateNote(self, event):
\r
578 Saves the note to the active file.
\r
580 active_file = self.GetActiveFile()
\r
581 active_file.note = self.panelNote.Editor.GetValue()
\r
583 def UpdateNote(self):
\r
584 #update the note for the active file
\r
585 active_file = self.GetActiveFile()
\r
586 if active_file is not None:
\r
587 self.panelNote.Editor.SetValue(active_file.note)
\r
589 def UpdatePlaylistsTreeSelection(self):
\r
590 playlist = self.GetActivePlaylist()
\r
591 if playlist is not None:
\r
592 if playlist.index >= 0:
\r
593 self._c['status bar'].set_playlist(playlist)
\r
597 def _on_curve_select(self, playlist, curve):
\r
598 #create the plot tab and add playlist to the dictionary
\r
599 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
600 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
601 #tab_index = self._c['notebook'].GetSelection()
\r
602 playlist.figure = plotPanel.get_figure()
\r
603 self.playlists[playlist.name] = playlist
\r
604 #self.playlists[playlist.name] = [playlist, figure]
\r
605 self._c['status bar'].set_playlist(playlist)
\r
610 def _on_playlist_left_doubleclick(self):
\r
611 index = self._c['notebook'].GetSelection()
\r
612 current_playlist = self._c['notebook'].GetPageText(index)
\r
613 if current_playlist != playlist_name:
\r
614 index = self._GetPlaylistTab(playlist_name)
\r
615 self._c['notebook'].SetSelection(index)
\r
616 self._c['status bar'].set_playlist(playlist)
\r
620 def _on_playlist_delete(self, playlist):
\r
621 notebook = self.Parent.plotNotebook
\r
622 index = self.Parent._GetPlaylistTab(playlist.name)
\r
623 notebook.SetSelection(index)
\r
624 notebook.DeletePage(notebook.GetSelection())
\r
625 self.Parent.DeleteFromPlaylists(playlist_name)
\r
629 # Command panel interface
\r
631 def select_command(self, _class, method, command):
\r
632 #self.select_plugin(plugin=command.plugin)
\r
633 if 'assistant' in self._c:
\r
634 self._c['assitant'].ChangeValue(command.help)
\r
635 self._c['property editor'].clear()
\r
636 for argument in command.arguments:
\r
637 if argument.name == 'help':
\r
640 results = self.execute_command(
\r
641 command=self._command_by_name('playlists'))
\r
642 if not isinstance(results[-1], Success):
\r
643 self._postprocess_text(command, results=results)
\r
646 playlists = results[0]
\r
648 results = self.execute_command(
\r
649 command=self._command_by_name('playlist curves'))
\r
650 if not isinstance(results[-1], Success):
\r
651 self._postprocess_text(command, results=results)
\r
654 curves = results[0]
\r
656 p = prop_from_argument(
\r
657 argument, curves=curves, playlists=playlists)
\r
659 continue # property intentionally not handled (yet)
\r
660 self._c['property editor'].append_property(p)
\r
662 self.gui.config['selected command'] = command # TODO: push to engine
\r
666 # Playlist panel interface
\r
668 def _on_user_delete_playlist(self, _class, method, playlist):
\r
671 def _on_delete_playlist(self, _class, method, playlist):
\r
672 if hasattr(playlist, 'path') and playlist.path != None:
\r
673 os.remove(playlist.path)
\r
675 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
678 def _on_delete_curve(self, _class, method, playlist, curve):
\r
679 os.remove(curve.path)
\r
681 def _on_set_selected_playlist(self, _class, method, playlist):
\r
682 """TODO: playlists plugin with `jump to playlist`.
\r
686 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
687 """Call the `jump to curve` command.
\r
689 TODO: playlists plugin.
\r
691 # TODO: jump to playlist, get playlist
\r
692 index = playlist.index(curve)
\r
693 results = self.execute_command(
\r
694 command=self._command_by_name('jump to curve'),
\r
695 args={'index':index})
\r
696 if not isinstance(results[-1], Success):
\r
698 #results = self.execute_command(
\r
699 # command=self._command_by_name('get playlist'))
\r
700 #if not isinstance(results[-1], Success):
\r
702 self.execute_command(
\r
703 command=self._command_by_name('get curve'))
\r
709 def _next_curve(self, *args):
\r
710 """Call the `next curve` command.
\r
712 results = self.execute_command(
\r
713 command=self._command_by_name('next curve'))
\r
714 if isinstance(results[-1], Success):
\r
715 self.execute_command(
\r
716 command=self._command_by_name('get curve'))
\r
718 def _previous_curve(self, *args):
\r
719 """Call the `previous curve` command.
\r
721 results = self.execute_command(
\r
722 command=self._command_by_name('previous curve'))
\r
723 if isinstance(results[-1], Success):
\r
724 self.execute_command(
\r
725 command=self._command_by_name('get curve'))
\r
729 # Panel display handling
\r
731 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
732 pane = self._c['manager'].GetPane(panel_name)
\r
735 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
736 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
737 #folders_size = pane.GetSize()
\r
738 self.panelFolders.Fit()
\r
739 self._c['manager'].Update()
\r
741 def _setup_perspectives(self):
\r
742 """Add perspectives to menubar and _perspectives.
\r
744 self._perspectives = {
\r
745 'Default': self._c['manager'].SavePerspective(),
\r
747 path = self.gui.config['perspective path']
\r
748 if os.path.isdir(path):
\r
749 files = sorted(os.listdir(path))
\r
750 for fname in files:
\r
751 name, extension = os.path.splitext(fname)
\r
752 if extension != self.gui.config['perspective extension']:
\r
754 fpath = os.path.join(path, fname)
\r
755 if not os.path.isfile(fpath):
\r
758 with open(fpath, 'rU') as f:
\r
759 perspective = f.readline()
\r
761 self._perspectives[name] = perspective
\r
763 selected_perspective = self.gui.config['active perspective']
\r
764 if not self._perspectives.has_key(selected_perspective):
\r
765 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
767 self._restore_perspective(selected_perspective, force=True)
\r
768 self._update_perspective_menu()
\r
770 def _update_perspective_menu(self):
\r
771 self._c['menu bar']._c['perspective'].update(
\r
772 sorted(self._perspectives.keys()),
\r
773 self.gui.config['active perspective'])
\r
775 def _save_perspective(self, perspective, perspective_dir, name,
\r
777 path = os.path.join(perspective_dir, name)
\r
778 if extension != None:
\r
780 if not os.path.isdir(perspective_dir):
\r
781 os.makedirs(perspective_dir)
\r
782 with open(path, 'w') as f:
\r
783 f.write(perspective)
\r
784 self._perspectives[name] = perspective
\r
785 self._restore_perspective(name)
\r
786 self._update_perspective_menu()
\r
788 def _delete_perspectives(self, perspective_dir, names,
\r
792 path = os.path.join(perspective_dir, name)
\r
793 if extension != None:
\r
796 del(self._perspectives[name])
\r
797 self._update_perspective_menu()
\r
798 if self.gui.config['active perspective'] in names:
\r
799 self._restore_perspective('Default')
\r
800 # TODO: does this bug still apply?
\r
801 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
802 # http://trac.wxwidgets.org/ticket/3258
\r
803 # ) that makes the radio item indicator in the menu disappear.
\r
804 # The code should be fine once this issue is fixed.
\r
806 def _restore_perspective(self, name, force=False):
\r
807 if name != self.gui.config['active perspective'] or force == True:
\r
808 print 'restoring perspective:', name
\r
809 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
810 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
811 self._c['manager'].Update()
\r
812 for pane in self._c['manager'].GetAllPanes():
\r
813 view = self._c['menu bar']._c['view']
\r
814 if pane.name in view._c.keys():
\r
815 view._c[pane.name].Check(pane.window.IsShown())
\r
817 def _on_save_perspective(self, *args):
\r
818 perspective = self._c['manager'].SavePerspective()
\r
819 name = self.gui.config['active perspective']
\r
820 if name == 'Default':
\r
821 name = 'New perspective'
\r
822 name = select_save_file(
\r
823 directory=self.gui.config['perspective path'],
\r
825 extension=self.gui.config['perspective extension'],
\r
827 message='Enter a name for the new perspective:',
\r
828 caption='Save perspective')
\r
831 self._save_perspective(
\r
832 perspective, self.gui.config['perspective path'], name=name,
\r
833 extension=self.gui.config['perspective extension'])
\r
835 def _on_delete_perspective(self, *args, **kwargs):
\r
836 options = sorted([p for p in self._perspectives.keys()
\r
837 if p != 'Default'])
\r
838 dialog = SelectionDialog(
\r
840 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
841 button_id=wx.ID_DELETE,
\r
842 selection_style='multiple',
\r
844 title='Delete perspective(s)',
\r
845 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
846 dialog.CenterOnScreen()
\r
848 names = [options[i] for i in dialog.selected]
\r
850 self._delete_perspectives(
\r
851 self.gui.config['perspective path'], names=names,
\r
852 extension=self.gui.config['perspective extension'])
\r
854 def _on_select_perspective(self, _class, method, name):
\r
855 self._restore_perspective(name)
\r
859 class HookeApp (wx.App):
\r
860 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
862 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
865 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
867 self.commands = commands
\r
868 self.inqueue = inqueue
\r
869 self.outqueue = outqueue
\r
870 super(HookeApp, self).__init__(*args, **kwargs)
\r
873 self.SetAppName('Hooke')
\r
874 self.SetVendorName('')
\r
875 self._setup_splash_screen()
\r
877 height = int(self.gui.config['main height']) # HACK: config should convert
\r
878 width = int(self.gui.config['main width'])
\r
879 top = int(self.gui.config['main top'])
\r
880 left = int(self.gui.config['main left'])
\r
882 # Sometimes, the ini file gets confused and sets 'left' and
\r
883 # 'top' to large negative numbers. Here we catch and fix
\r
884 # this. Keep small negative numbers, the user might want
\r
892 'frame': HookeFrame(
\r
893 self.gui, self.commands, self.inqueue, self.outqueue,
\r
894 parent=None, title='Hooke',
\r
895 pos=(left, top), size=(width, height),
\r
896 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
898 self._c['frame'].Show(True)
\r
899 self.SetTopWindow(self._c['frame'])
\r
902 def _setup_splash_screen(self):
\r
903 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
904 print 'splash', self.gui.config['show splash screen']
\r
905 path = self.gui.config['splash screen image']
\r
906 if os.path.isfile(path):
\r
907 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
909 bitmap=wx.Image(path).ConvertToBitmap(),
\r
910 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
911 milliseconds=duration,
\r
914 # For some reason splashDuration and sleep do not
\r
915 # correspond to each other at least not on Windows.
\r
916 # Maybe it's because duration is in milliseconds and
\r
917 # sleep in seconds. Thus we need to increase the
\r
918 # sleep time a bit. A factor of 1.2 seems to work.
\r
920 time.sleep(sleepFactor * duration / 1000)
\r
923 class GUI (UserInterface):
\r
924 """wxWindows graphical user interface.
\r
926 def __init__(self):
\r
927 super(GUI, self).__init__(name='gui')
\r
929 def default_settings(self):
\r
930 """Return a list of :class:`hooke.config.Setting`\s for any
\r
931 configurable UI settings.
\r
933 The suggested section setting is::
\r
935 Setting(section=self.setting_section, help=self.__doc__)
\r
938 Setting(section=self.setting_section, help=self.__doc__),
\r
939 Setting(section=self.setting_section, option='icon image',
\r
940 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
941 help='Path to the hooke icon image.'),
\r
942 Setting(section=self.setting_section, option='show splash screen',
\r
944 help='Enable/disable the splash screen'),
\r
945 Setting(section=self.setting_section, option='splash screen image',
\r
946 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
947 help='Path to the Hooke splash screen image.'),
\r
948 Setting(section=self.setting_section, option='splash screen duration',
\r
950 help='Duration of the splash screen in milliseconds.'),
\r
951 Setting(section=self.setting_section, option='perspective path',
\r
952 value=os.path.join('resources', 'gui', 'perspective'),
\r
953 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
954 Setting(section=self.setting_section, option='perspective extension',
\r
956 help='Extension for perspective files.'),
\r
957 Setting(section=self.setting_section, option='hide extensions',
\r
959 help='Hide file extensions when displaying names.'),
\r
960 Setting(section=self.setting_section, option='plot legend',
\r
962 help='Enable/disable the plot legend.'),
\r
963 Setting(section=self.setting_section, option='plot x format',
\r
965 help='Display format for plot x values.'),
\r
966 Setting(section=self.setting_section, option='plot y format',
\r
968 help='Display format for plot y values.'),
\r
969 Setting(section=self.setting_section, option='plot zero',
\r
971 help='Select "0" vs. e.g. "0.00" for plot axes?'),
\r
972 Setting(section=self.setting_section, option='folders-workdir',
\r
974 help='This should probably go...'),
\r
975 Setting(section=self.setting_section, option='folders-filters',
\r
977 help='This should probably go...'),
\r
978 Setting(section=self.setting_section, option='active perspective',
\r
980 help='Name of active perspective file (or "Default").'),
\r
981 Setting(section=self.setting_section, option='folders-filter-index',
\r
983 help='This should probably go...'),
\r
984 Setting(section=self.setting_section, option='main height',
\r
986 help='Height of main window in pixels.'),
\r
987 Setting(section=self.setting_section, option='main width',
\r
989 help='Width of main window in pixels.'),
\r
990 Setting(section=self.setting_section, option='main top',
\r
992 help='Pixels from screen top to top of main window.'),
\r
993 Setting(section=self.setting_section, option='main left',
\r
995 help='Pixels from screen left to left of main window.'),
\r
996 Setting(section=self.setting_section, option='selected command',
\r
997 value='load playlist',
\r
998 help='Name of the initially selected command.'),
\r
1001 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1005 app = HookeApp(gui=self,
\r
1006 commands=commands,
\r
1007 inqueue=ui_to_command_queue,
\r
1008 outqueue=command_to_ui_queue,
\r
1009 redirect=redirect)
\r
1012 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1013 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r