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 statusbar as statusbar
\r
40 class HookeFrame (wx.Frame):
\r
41 """The main Hooke-interface window.
\r
43 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
44 super(HookeFrame, self).__init__(*args, **kwargs)
\r
46 self.commands = commands
\r
47 self.inqueue = inqueue
\r
48 self.outqueue = outqueue
\r
49 self._perspectives = {} # {name: perspective_str}
\r
52 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
54 # setup frame manager
\r
55 self._c['manager'] = aui.AuiManager()
\r
56 self._c['manager'].SetManagedWindow(self)
\r
58 # set the gradient and drag styles
\r
59 self._c['manager'].GetArtProvider().SetMetric(
\r
60 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
61 self._c['manager'].SetFlags(
\r
62 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
64 # Min size for the frame itself isn't completely done. See
\r
65 # the end of FrameManager::Update() for the test code. For
\r
66 # now, just hard code a frame minimum size.
\r
67 #self.SetMinSize(wx.Size(500, 500))
\r
69 self._setup_panels()
\r
70 self._setup_toolbars()
\r
71 self._c['manager'].Update() # commit pending changes
\r
73 # Create the menubar after the panes so that the default
\r
74 # perspective is created with all panes open
\r
75 panels = [p for p in self._c.values() if isinstance(p, panel.Panel)]
\r
76 self._c['menu bar'] = menu.HookeMenuBar(
\r
80 'close': self._on_close,
\r
81 'about': self._on_about,
\r
82 'view_panel': self._on_panel_visibility,
\r
83 'save_perspective': self._on_save_perspective,
\r
84 'delete_perspective': self._on_delete_perspective,
\r
85 'select_perspective': self._on_select_perspective,
\r
87 self.SetMenuBar(self._c['menu bar'])
\r
89 self._c['status bar'] = statusbar.StatusBar(
\r
91 style=wx.ST_SIZEGRIP)
\r
92 self.SetStatusBar(self._c['status bar'])
\r
94 self._setup_perspectives()
\r
97 self.execute_command(
\r
98 command=self._command_by_name('load playlist'),
\r
99 args={'input':'test/data/test'},
\r
101 return # TODO: cleanup
\r
102 self.playlists = self._c['playlist'].Playlists
\r
103 self._displayed_plot = None
\r
104 #load default list, if possible
\r
105 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlists'))
\r
110 def _setup_panels(self):
\r
111 client_size = self.GetClientSize()
\r
113 # ('folders', wx.GenericDirCtrl(
\r
115 # dir=self.gui.config['folders-workdir'],
\r
117 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
118 # filter=self.gui.config['folders-filters'],
\r
119 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
120 (panel.PANELS['playlist'](
\r
122 'delete_playlist':self._on_user_delete_playlist,
\r
123 '_delete_playlist':self._on_delete_playlist,
\r
124 'delete_curve':self._on_user_delete_curve,
\r
125 '_delete_curve':self._on_delete_curve,
\r
126 '_on_set_selected_playlist':self._on_set_selected_playlist,
\r
127 '_on_set_selected_curve':self._on_set_selected_curve,
\r
130 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
131 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
134 # ('note', panel.note.Note(
\r
136 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
137 # size=(160, 200)), 'left'),
\r
138 # ('notebook', Notebook(
\r
140 # pos=wx.Point(client_size.x, client_size.y),
\r
141 # size=wx.Size(430, 200),
\r
142 # style=aui.AUI_NB_DEFAULT_STYLE
\r
143 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
144 (panel.PANELS['commands'](
\r
145 commands=self.commands,
\r
146 selected=self.gui.config['selected command'],
\r
148 'execute': self.execute_command,
\r
149 'select_plugin': self.select_plugin,
\r
150 'select_command': self.select_command,
\r
151 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
154 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
155 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
158 (panel.PANELS['propertyeditor'](
\r
161 style=wx.WANTS_CHARS,
\r
162 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
164 # ('assistant', wx.TextCtrl(
\r
166 # pos=wx.Point(0, 0),
\r
167 # size=wx.Size(150, 90),
\r
168 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
169 (panel.PANELS['plot'](
\r
173 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
174 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
177 (panel.PANELS['output'](
\r
179 pos=wx.Point(0, 0),
\r
180 size=wx.Size(150, 90),
\r
181 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
183 # ('results', panel.results.Results(self), 'bottom'),
\r
185 self._add_panel(p, style)
\r
186 #self._c['assistant'].SetEditable(False)
\r
188 def _add_panel(self, panel, style):
\r
189 self._c[panel.name] = panel
\r
190 m_name = panel.managed_name
\r
191 info = aui.AuiPaneInfo().Name(m_name).Caption(m_name)
\r
192 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
195 elif style == 'center':
\r
197 elif style == 'left':
\r
199 elif style == 'right':
\r
202 assert style == 'bottom', style
\r
204 self._c['manager'].AddPane(panel, info)
\r
206 def _setup_toolbars(self):
\r
207 self._c['navigation bar'] = navbar.NavBar(
\r
209 'next': self._next_curve,
\r
210 'previous': self._previous_curve,
\r
213 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
214 self._c['manager'].AddPane(
\r
215 self._c['navigation bar'],
\r
216 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
217 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
218 ).RightDockable(False))
\r
220 def _bind_events(self):
\r
221 # TODO: figure out if we can use the eventManager for menu
\r
222 # ranges and events of 'self' without raising an assertion
\r
224 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
225 self.Bind(wx.EVT_SIZE, self._on_size)
\r
226 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
227 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
228 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
230 return # TODO: cleanup
\r
231 for value in self._c['menu bar']._c['view']._c.values():
\r
232 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
234 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
235 self._c['menu bar']._c['perspective']._c['save'])
\r
236 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
237 self._c['menu bar']._c['perspective']._c['delete'])
\r
239 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
240 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
242 # TODO: playlist callbacks
\r
243 return # TODO: cleanup
\r
244 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
246 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
248 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
250 def _on_about(self, *args):
\r
251 dialog = wx.MessageDialog(
\r
253 message=self.gui._splash_text(extra_info={
\r
254 'get-details':'click "Help -> License"'},
\r
256 caption='About Hooke',
\r
257 style=wx.OK|wx.ICON_INFORMATION)
\r
261 def _on_close(self, *args):
\r
263 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
264 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
265 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
266 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
267 # push changes back to Hooke.config?
\r
268 self._c['manager'].UnInit()
\r
269 del self._c['manager']
\r
274 # Panel utility functions
\r
276 def _file_name(self, name):
\r
277 """Cleanup names according to configured preferences.
\r
279 if self.gui.config['hide extensions'] == 'True': # HACK: config should decode
\r
280 name,ext = os.path.splitext(name)
\r
287 def _command_by_name(self, name):
\r
288 cs = [c for c in self.commands if c.name == name]
\r
290 raise KeyError(name)
\r
292 raise Exception('Multiple commands named "%s"' % name)
\r
295 def execute_command(self, _class=None, method=None,
\r
296 command=None, args=None):
\r
299 if ('property editor' in self._c
\r
300 and self.gui.config['selected command'] == command):
\r
301 arg_names = [arg.name for arg in command.arguments]
\r
302 for name,value in self._c['property editor'].get_values().items():
\r
303 if name in arg_names:
\r
305 print 'executing', command.name, args
\r
306 self.inqueue.put(CommandMessage(command, args))
\r
309 msg = self.outqueue.get()
\r
310 results.append(msg)
\r
311 if isinstance(msg, Exit):
\r
314 elif isinstance(msg, CommandExit):
\r
315 # TODO: display command complete
\r
317 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
318 self.gui.reload_config(msg.config)
\r
320 elif isinstance(msg, Request):
\r
321 h = handler.HANDLERS[msg.type]
\r
322 h.run(self, msg) # TODO: pause for response?
\r
325 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
326 self._postprocess_text)
\r
327 pp(command=command, args=args, results=results)
\r
330 def _handle_request(self, msg):
\r
331 """Repeatedly try to get a response to `msg`.
\r
334 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
335 prompt_string = prompt(msg)
\r
336 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
338 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
342 self.cmd.stdout.write(''.join([
\r
343 error.__class__.__name__, ': ', str(error), '\n']))
\r
344 self.cmd.stdout.write(prompt_string)
\r
345 value = parser(msg, self.cmd.stdin.readline())
\r
347 response = msg.response(value)
\r
349 except ValueError, error:
\r
351 self.inqueue.put(response)
\r
355 # Command-specific postprocessing
\r
357 def _postprocess_text(self, command, args={}, results=[]):
\r
358 """Print the string representation of the results to the Results window.
\r
360 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
361 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
362 doesn't print some internally handled messages
\r
363 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
365 for result in results:
\r
366 if isinstance(result, CommandExit):
\r
367 self._c['output'].write(result.__class__.__name__+'\n')
\r
368 self._c['output'].write(str(result).rstrip()+'\n')
\r
370 def _postprocess_load_playlist(self, command, args={}, results=None):
\r
371 """Update `self` to show the playlist.
\r
373 if not isinstance(results[-1], Success):
\r
374 self._postprocess_text(command, results=results)
\r
376 assert len(results) == 2, results
\r
377 playlist = results[0]
\r
378 self._c['playlist']._c['tree'].add_playlist(playlist)
\r
380 def _postprocess_get_playlist(self, command, args={}, results=[]):
\r
381 if not isinstance(results[-1], Success):
\r
382 self._postprocess_text(command, results=results)
\r
384 assert len(results) == 2, results
\r
385 playlist = results[0]
\r
386 self._c['playlist']._c['tree'].update_playlist(playlist)
\r
388 def _postprocess_get_curve(self, command, args={}, results=[]):
\r
389 """Update `self` to show the curve.
\r
391 if not isinstance(results[-1], Success):
\r
392 self._postprocess_text(command, results=results)
\r
394 assert len(results) == 2, results
\r
396 if args.get('curve', None) == None:
\r
397 # the command defaults to the current curve of the current playlist
\r
398 results = self.execute_command(
\r
399 command=self._command_by_name('get playlist'))
\r
400 playlist = results[0]
\r
402 raise NotImplementedError()
\r
403 if 'playlist' in self._c:
\r
404 self._c['playlist']._c['tree'].set_selected_curve(
\r
406 if 'plot' in self._c:
\r
407 self._c['plot'].set_curve(curve, config=self.gui.config)
\r
409 def _postprocess_next_curve(self, command, args={}, results=[]):
\r
410 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
414 def _postprocess_previous_curve(self, command, args={}, results=[]):
\r
415 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
422 def _GetActiveFileIndex(self):
\r
423 lib.playlist.Playlist = self.GetActivePlaylist()
\r
424 #get the selected item from the tree
\r
425 selected_item = self._c['playlist']._c['tree'].GetSelection()
\r
426 #test if a playlist or a curve was double-clicked
\r
427 if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
\r
431 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
432 while selected_item.IsOk():
\r
434 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
\r
437 def _GetPlaylistTab(self, name):
\r
438 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
439 if page.caption == name:
\r
443 def select_plugin(self, _class=None, method=None, plugin=None):
\r
446 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
448 playlist = lib.playlist.Playlist(self, self.drivers)
\r
450 playlist.add_curve(item)
\r
451 if playlist.count > 0:
\r
452 playlist.name = self._GetUniquePlaylistName(name)
\r
454 self.AddTayliss(playlist)
\r
456 def AppliesPlotmanipulator(self, name):
\r
458 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
459 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
461 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
463 def ApplyPlotmanipulators(self, plot, plot_file):
\r
465 Apply all active plotmanipulators.
\r
467 if plot is not None and plot_file is not None:
\r
468 manipulated_plot = copy.deepcopy(plot)
\r
469 for plotmanipulator in self.plotmanipulators:
\r
470 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
471 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
472 return manipulated_plot
\r
474 def GetActiveFigure(self):
\r
475 playlist_name = self.GetActivePlaylistName()
\r
476 figure = self.playlists[playlist_name].figure
\r
477 if figure is not None:
\r
481 def GetActiveFile(self):
\r
482 playlist = self.GetActivePlaylist()
\r
483 if playlist is not None:
\r
484 return playlist.get_active_file()
\r
487 def GetActivePlot(self):
\r
488 playlist = self.GetActivePlaylist()
\r
489 if playlist is not None:
\r
490 return playlist.get_active_file().plot
\r
493 def GetDisplayedPlot(self):
\r
494 plot = copy.deepcopy(self.displayed_plot)
\r
496 #plot.curves = copy.deepcopy(plot.curves)
\r
499 def GetDisplayedPlotCorrected(self):
\r
500 plot = copy.deepcopy(self.displayed_plot)
\r
502 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
505 def GetDisplayedPlotRaw(self):
\r
506 plot = copy.deepcopy(self.displayed_plot)
\r
508 plot.curves = copy.deepcopy(plot.raw_curves)
\r
511 def GetDockArt(self):
\r
512 return self._c['manager'].GetArtProvider()
\r
514 def GetPlotmanipulator(self, name):
\r
516 Returns a plot manipulator function from its name
\r
518 for plotmanipulator in self.plotmanipulators:
\r
519 if plotmanipulator.name == name:
\r
520 return plotmanipulator
\r
523 def HasPlotmanipulator(self, name):
\r
525 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
527 for plotmanipulator in self.plotmanipulators:
\r
528 if plotmanipulator.command == name:
\r
533 def _on_dir_ctrl_left_double_click(self, event):
\r
534 file_path = self.panelFolders.GetPath()
\r
535 if os.path.isfile(file_path):
\r
536 if file_path.endswith('.hkp'):
\r
537 self.do_loadlist(file_path)
\r
540 def _on_erase_background(self, event):
\r
543 def _on_notebook_page_close(self, event):
\r
544 ctrl = event.GetEventObject()
\r
545 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
546 self.DeleteFromPlaylists(playlist_name)
\r
548 def OnPaneClose(self, event):
\r
551 def OnPropGridChanged (self, event):
\r
552 prop = event.GetProperty()
\r
554 item_section = self.panelProperties.SelectedTreeItem
\r
555 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
556 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
557 config = self.gui.config[plugin]
\r
558 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
559 property_key = prop.GetName()
\r
560 property_value = prop.GetDisplayedString()
\r
562 config[property_section][property_key]['value'] = property_value
\r
564 def OnResultsCheck(self, index, flag):
\r
565 results = self.GetActivePlot().results
\r
566 if results.has_key(self.results_str):
\r
567 results[self.results_str].results[index].visible = flag
\r
568 results[self.results_str].update()
\r
572 def _on_size(self, event):
\r
575 def OnUpdateNote(self, event):
\r
577 Saves the note to the active file.
\r
579 active_file = self.GetActiveFile()
\r
580 active_file.note = self.panelNote.Editor.GetValue()
\r
582 def UpdateNote(self):
\r
583 #update the note for the active file
\r
584 active_file = self.GetActiveFile()
\r
585 if active_file is not None:
\r
586 self.panelNote.Editor.SetValue(active_file.note)
\r
588 def UpdatePlaylistsTreeSelection(self):
\r
589 playlist = self.GetActivePlaylist()
\r
590 if playlist is not None:
\r
591 if playlist.index >= 0:
\r
592 self._c['status bar'].set_playlist(playlist)
\r
596 def _on_curve_select(self, playlist, curve):
\r
597 #create the plot tab and add playlist to the dictionary
\r
598 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
599 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
600 #tab_index = self._c['notebook'].GetSelection()
\r
601 playlist.figure = plotPanel.get_figure()
\r
602 self.playlists[playlist.name] = playlist
\r
603 #self.playlists[playlist.name] = [playlist, figure]
\r
604 self._c['status bar'].set_playlist(playlist)
\r
609 def _on_playlist_left_doubleclick(self):
\r
610 index = self._c['notebook'].GetSelection()
\r
611 current_playlist = self._c['notebook'].GetPageText(index)
\r
612 if current_playlist != playlist_name:
\r
613 index = self._GetPlaylistTab(playlist_name)
\r
614 self._c['notebook'].SetSelection(index)
\r
615 self._c['status bar'].set_playlist(playlist)
\r
619 def _on_playlist_delete(self, playlist):
\r
620 notebook = self.Parent.plotNotebook
\r
621 index = self.Parent._GetPlaylistTab(playlist.name)
\r
622 notebook.SetSelection(index)
\r
623 notebook.DeletePage(notebook.GetSelection())
\r
624 self.Parent.DeleteFromPlaylists(playlist_name)
\r
628 # Command panel interface
\r
630 def select_command(self, _class, method, command):
\r
631 #self.select_plugin(plugin=command.plugin)
\r
632 if 'assistant' in self._c:
\r
633 self._c['assitant'].ChangeValue(command.help)
\r
634 self._c['property editor'].clear()
\r
635 for argument in command.arguments:
\r
636 if argument.name == 'help':
\r
639 results = self.execute_command(
\r
640 command=self._command_by_name('playlists'))
\r
641 if not isinstance(results[-1], Success):
\r
642 self._postprocess_text(command, results=results)
\r
645 playlists = results[0]
\r
647 results = self.execute_command(
\r
648 command=self._command_by_name('playlist curves'))
\r
649 if not isinstance(results[-1], Success):
\r
650 self._postprocess_text(command, results=results)
\r
653 curves = results[0]
\r
655 p = prop_from_argument(
\r
656 argument, curves=curves, playlists=playlists)
\r
658 continue # property intentionally not handled (yet)
\r
659 self._c['property editor'].append_property(p)
\r
661 self.gui.config['selected command'] = command # TODO: push to engine
\r
665 # Playlist panel interface
\r
667 def _on_user_delete_playlist(self, _class, method, playlist):
\r
670 def _on_delete_playlist(self, _class, method, playlist):
\r
671 if hasattr(playlist, 'path') and playlist.path != None:
\r
672 os.remove(playlist.path)
\r
674 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
677 def _on_delete_curve(self, _class, method, playlist, curve):
\r
678 os.remove(curve.path)
\r
680 def _on_set_selected_playlist(self, _class, method, playlist):
\r
681 """TODO: playlists plugin with `jump to playlist`.
\r
685 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
686 """Call the `jump to curve` command.
\r
688 TODO: playlists plugin.
\r
690 # TODO: jump to playlist, get playlist
\r
691 index = playlist.index(curve)
\r
692 results = self.execute_command(
\r
693 command=self._command_by_name('jump to curve'),
\r
694 args={'index':index})
\r
695 if not isinstance(results[-1], Success):
\r
697 #results = self.execute_command(
\r
698 # command=self._command_by_name('get playlist'))
\r
699 #if not isinstance(results[-1], Success):
\r
701 self.execute_command(
\r
702 command=self._command_by_name('get curve'))
\r
708 def _next_curve(self, *args):
\r
709 """Call the `next curve` command.
\r
711 results = self.execute_command(
\r
712 command=self._command_by_name('next curve'))
\r
713 if isinstance(results[-1], Success):
\r
714 self.execute_command(
\r
715 command=self._command_by_name('get curve'))
\r
717 def _previous_curve(self, *args):
\r
718 """Call the `previous curve` command.
\r
720 results = self.execute_command(
\r
721 command=self._command_by_name('previous curve'))
\r
722 if isinstance(results[-1], Success):
\r
723 self.execute_command(
\r
724 command=self._command_by_name('get curve'))
\r
728 # Panel display handling
\r
730 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
731 pane = self._c['manager'].GetPane(panel_name)
\r
734 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
735 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
736 #folders_size = pane.GetSize()
\r
737 self.panelFolders.Fit()
\r
738 self._c['manager'].Update()
\r
740 def _setup_perspectives(self):
\r
741 """Add perspectives to menubar and _perspectives.
\r
743 self._perspectives = {
\r
744 'Default': self._c['manager'].SavePerspective(),
\r
746 path = self.gui.config['perspective path']
\r
747 if os.path.isdir(path):
\r
748 files = sorted(os.listdir(path))
\r
749 for fname in files:
\r
750 name, extension = os.path.splitext(fname)
\r
751 if extension != self.gui.config['perspective extension']:
\r
753 fpath = os.path.join(path, fname)
\r
754 if not os.path.isfile(fpath):
\r
757 with open(fpath, 'rU') as f:
\r
758 perspective = f.readline()
\r
760 self._perspectives[name] = perspective
\r
762 selected_perspective = self.gui.config['active perspective']
\r
763 if not self._perspectives.has_key(selected_perspective):
\r
764 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
766 self._restore_perspective(selected_perspective, force=True)
\r
767 self._update_perspective_menu()
\r
769 def _update_perspective_menu(self):
\r
770 self._c['menu bar']._c['perspective'].update(
\r
771 sorted(self._perspectives.keys()),
\r
772 self.gui.config['active perspective'])
\r
774 def _save_perspective(self, perspective, perspective_dir, name,
\r
776 path = os.path.join(perspective_dir, name)
\r
777 if extension != None:
\r
779 if not os.path.isdir(perspective_dir):
\r
780 os.makedirs(perspective_dir)
\r
781 with open(path, 'w') as f:
\r
782 f.write(perspective)
\r
783 self._perspectives[name] = perspective
\r
784 self._restore_perspective(name)
\r
785 self._update_perspective_menu()
\r
787 def _delete_perspectives(self, perspective_dir, names,
\r
791 path = os.path.join(perspective_dir, name)
\r
792 if extension != None:
\r
795 del(self._perspectives[name])
\r
796 self._update_perspective_menu()
\r
797 if self.gui.config['active perspective'] in names:
\r
798 self._restore_perspective('Default')
\r
799 # TODO: does this bug still apply?
\r
800 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
801 # http://trac.wxwidgets.org/ticket/3258
\r
802 # ) that makes the radio item indicator in the menu disappear.
\r
803 # The code should be fine once this issue is fixed.
\r
805 def _restore_perspective(self, name, force=False):
\r
806 if name != self.gui.config['active perspective'] or force == True:
\r
807 print 'restoring perspective:', name
\r
808 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
809 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
810 self._c['manager'].Update()
\r
811 for pane in self._c['manager'].GetAllPanes():
\r
812 view = self._c['menu bar']._c['view']
\r
813 if pane.name in view._c.keys():
\r
814 view._c[pane.name].Check(pane.window.IsShown())
\r
816 def _on_save_perspective(self, *args):
\r
817 perspective = self._c['manager'].SavePerspective()
\r
818 name = self.gui.config['active perspective']
\r
819 if name == 'Default':
\r
820 name = 'New perspective'
\r
821 name = select_save_file(
\r
822 directory=self.gui.config['perspective path'],
\r
824 extension=self.gui.config['perspective extension'],
\r
826 message='Enter a name for the new perspective:',
\r
827 caption='Save perspective')
\r
830 self._save_perspective(
\r
831 perspective, self.gui.config['perspective path'], name=name,
\r
832 extension=self.gui.config['perspective extension'])
\r
834 def _on_delete_perspective(self, *args, **kwargs):
\r
835 options = sorted([p for p in self._perspectives.keys()
\r
836 if p != 'Default'])
\r
837 dialog = SelectionDialog(
\r
839 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
840 button_id=wx.ID_DELETE,
\r
841 selection_style='multiple',
\r
843 title='Delete perspective(s)',
\r
844 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
845 dialog.CenterOnScreen()
\r
847 names = [options[i] for i in dialog.selected]
\r
849 self._delete_perspectives(
\r
850 self.gui.config['perspective path'], names=names,
\r
851 extension=self.gui.config['perspective extension'])
\r
853 def _on_select_perspective(self, _class, method, name):
\r
854 self._restore_perspective(name)
\r
858 class HookeApp (wx.App):
\r
859 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
861 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
864 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
866 self.commands = commands
\r
867 self.inqueue = inqueue
\r
868 self.outqueue = outqueue
\r
869 super(HookeApp, self).__init__(*args, **kwargs)
\r
872 self.SetAppName('Hooke')
\r
873 self.SetVendorName('')
\r
874 self._setup_splash_screen()
\r
876 height = int(self.gui.config['main height']) # HACK: config should convert
\r
877 width = int(self.gui.config['main width'])
\r
878 top = int(self.gui.config['main top'])
\r
879 left = int(self.gui.config['main left'])
\r
881 # Sometimes, the ini file gets confused and sets 'left' and
\r
882 # 'top' to large negative numbers. Here we catch and fix
\r
883 # this. Keep small negative numbers, the user might want
\r
891 'frame': HookeFrame(
\r
892 self.gui, self.commands, self.inqueue, self.outqueue,
\r
893 parent=None, title='Hooke',
\r
894 pos=(left, top), size=(width, height),
\r
895 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
897 self._c['frame'].Show(True)
\r
898 self.SetTopWindow(self._c['frame'])
\r
901 def _setup_splash_screen(self):
\r
902 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
903 print 'splash', self.gui.config['show splash screen']
\r
904 path = self.gui.config['splash screen image']
\r
905 if os.path.isfile(path):
\r
906 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
908 bitmap=wx.Image(path).ConvertToBitmap(),
\r
909 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
910 milliseconds=duration,
\r
913 # For some reason splashDuration and sleep do not
\r
914 # correspond to each other at least not on Windows.
\r
915 # Maybe it's because duration is in milliseconds and
\r
916 # sleep in seconds. Thus we need to increase the
\r
917 # sleep time a bit. A factor of 1.2 seems to work.
\r
919 time.sleep(sleepFactor * duration / 1000)
\r
922 class GUI (UserInterface):
\r
923 """wxWindows graphical user interface.
\r
925 def __init__(self):
\r
926 super(GUI, self).__init__(name='gui')
\r
928 def default_settings(self):
\r
929 """Return a list of :class:`hooke.config.Setting`\s for any
\r
930 configurable UI settings.
\r
932 The suggested section setting is::
\r
934 Setting(section=self.setting_section, help=self.__doc__)
\r
937 Setting(section=self.setting_section, help=self.__doc__),
\r
938 Setting(section=self.setting_section, option='icon image',
\r
939 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
940 help='Path to the hooke icon image.'),
\r
941 Setting(section=self.setting_section, option='show splash screen',
\r
943 help='Enable/disable the splash screen'),
\r
944 Setting(section=self.setting_section, option='splash screen image',
\r
945 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
946 help='Path to the Hooke splash screen image.'),
\r
947 Setting(section=self.setting_section, option='splash screen duration',
\r
949 help='Duration of the splash screen in milliseconds.'),
\r
950 Setting(section=self.setting_section, option='perspective path',
\r
951 value=os.path.join('resources', 'gui', 'perspective'),
\r
952 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
953 Setting(section=self.setting_section, option='perspective extension',
\r
955 help='Extension for perspective files.'),
\r
956 Setting(section=self.setting_section, option='hide extensions',
\r
958 help='Hide file extensions when displaying names.'),
\r
959 Setting(section=self.setting_section, option='plot legend',
\r
961 help='Enable/disable the plot legend.'),
\r
962 Setting(section=self.setting_section, option='plot SI format',
\r
964 help='Enable/disable SI plot axes numbering.'),
\r
965 Setting(section=self.setting_section, option='plot decimals',
\r
967 help='Number of decimal places to show if "plot SI format" is enabled.'),
\r
968 Setting(section=self.setting_section, option='folders-workdir',
\r
970 help='This should probably go...'),
\r
971 Setting(section=self.setting_section, option='folders-filters',
\r
973 help='This should probably go...'),
\r
974 Setting(section=self.setting_section, option='active perspective',
\r
976 help='Name of active perspective file (or "Default").'),
\r
977 Setting(section=self.setting_section, option='folders-filter-index',
\r
979 help='This should probably go...'),
\r
980 Setting(section=self.setting_section, option='main height',
\r
982 help='Height of main window in pixels.'),
\r
983 Setting(section=self.setting_section, option='main width',
\r
985 help='Width of main window in pixels.'),
\r
986 Setting(section=self.setting_section, option='main top',
\r
988 help='Pixels from screen top to top of main window.'),
\r
989 Setting(section=self.setting_section, option='main left',
\r
991 help='Pixels from screen left to left of main window.'),
\r
992 Setting(section=self.setting_section, option='selected command',
\r
993 value='load playlist',
\r
994 help='Name of the initially selected command.'),
\r
997 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1001 app = HookeApp(gui=self,
\r
1002 commands=commands,
\r
1003 inqueue=ui_to_command_queue,
\r
1004 outqueue=command_to_ui_queue,
\r
1005 redirect=redirect)
\r
1008 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1009 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r