3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
\r
9 wxversion.select(WX_GOOD)
\r
19 import wx.aui as aui
\r
20 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 matplotlib.ticker import FuncFormatter
\r
30 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
\r
31 from ...config import Setting
\r
32 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
33 from ...ui import UserInterface, CommandMessage
\r
34 from .dialog.selection import Selection as SelectionDialog
\r
35 from .dialog.save_file import select_save_file
\r
36 from . import menu as menu
\r
37 from . import navbar as navbar
\r
38 from . import panel as panel
\r
39 from .panel.propertyeditor2 import prop_from_argument, prop_from_setting
\r
40 from . import prettyformat as prettyformat
\r
41 from . import statusbar as statusbar
\r
44 class HookeFrame (wx.Frame):
\r
45 """The main Hooke-interface window.
\r
47 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
48 super(HookeFrame, self).__init__(*args, **kwargs)
\r
50 self.commands = commands
\r
51 self.inqueue = inqueue
\r
52 self.outqueue = outqueue
\r
53 self._perspectives = {} # {name: perspective_str}
\r
56 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
58 # setup frame manager
\r
59 self._c['manager'] = aui.AuiManager()
\r
60 self._c['manager'].SetManagedWindow(self)
\r
62 # set the gradient and drag styles
\r
63 self._c['manager'].GetArtProvider().SetMetric(
\r
64 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
65 self._c['manager'].SetFlags(
\r
66 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
68 # Min size for the frame itself isn't completely done. See
\r
69 # the end of FrameManager::Update() for the test code. For
\r
70 # now, just hard code a frame minimum size.
\r
71 self.SetMinSize(wx.Size(500, 500))
\r
73 self._setup_panels()
\r
74 self._setup_toolbars()
\r
75 self._c['manager'].Update() # commit pending changes
\r
77 # Create the menubar after the panes so that the default
\r
78 # perspective is created with all panes open
\r
79 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 name = self.gui.config['active perspective']
\r
100 return # TODO: cleanup
\r
101 self.playlists = self._c['playlists'].Playlists
\r
102 self._displayed_plot = None
\r
103 #load default list, if possible
\r
104 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
109 def _setup_panels(self):
\r
110 client_size = self.GetClientSize()
\r
111 for label,p,style in [
\r
112 # ('folders', wx.GenericDirCtrl(
\r
114 # dir=self.gui.config['folders-workdir'],
\r
116 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
117 # filter=self.gui.config['folders-filters'],
\r
118 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
119 ('playlists', panel.PANELS['playlist'](
\r
121 'delete_playlist':self._on_user_delete_playlist,
\r
122 '_delete_playlist':self._on_delete_playlist,
\r
123 'delete_curve':self._on_user_delete_curve,
\r
124 '_delete_curve':self._on_delete_curve,
\r
125 '_on_set_selected_playlist':self._on_set_selected_playlist,
\r
126 '_on_set_selected_curve':self._on_set_selected_curve,
\r
128 config=self.gui.config,
\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 ('commands', 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 ('property', panel.PANELS['propertyeditor2'](
\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 ('output', panel.PANELS['output'](
\r
171 pos=wx.Point(0, 0),
\r
172 size=wx.Size(150, 90),
\r
173 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
175 # ('results', panel.results.Results(self), 'bottom'),
\r
177 self._add_panel(label, p, style)
\r
178 #self._c['assistant'].SetEditable(False)
\r
180 def _add_panel(self, label, panel, style):
\r
181 self._c[label] = panel
\r
182 cap_label = label.capitalize()
\r
183 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
184 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
187 elif style == 'center':
\r
189 elif style == 'left':
\r
191 elif style == 'right':
\r
194 assert style == 'bottom', style
\r
196 self._c['manager'].AddPane(panel, info)
\r
198 def _setup_toolbars(self):
\r
199 self._c['navigation bar'] = navbar.NavBar(
\r
201 'next': self._next_curve,
\r
202 'previous': self._previous_curve,
\r
205 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
206 self._c['manager'].AddPane(
\r
207 self._c['navigation bar'],
\r
208 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
209 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
210 ).RightDockable(False))
\r
212 def _bind_events(self):
\r
213 # TODO: figure out if we can use the eventManager for menu
\r
214 # ranges and events of 'self' without raising an assertion
\r
216 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
217 self.Bind(wx.EVT_SIZE, self._on_size)
\r
218 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
219 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
220 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
222 return # TODO: cleanup
\r
223 for value in self._c['menu bar']._c['view']._c.values():
\r
224 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
226 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
227 self._c['menu bar']._c['perspective']._c['save'])
\r
228 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
229 self._c['menu bar']._c['perspective']._c['delete'])
\r
231 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
232 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
234 # TODO: playlist callbacks
\r
235 return # TODO: cleanup
\r
236 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
238 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
240 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
242 def _on_about(self, *args):
\r
243 dialog = wx.MessageDialog(
\r
245 message=self.gui._splash_text(extra_info={
\r
246 'get-details':'click "Help -> License"',
\r
248 caption='About Hooke',
\r
249 style=wx.OK|wx.ICON_INFORMATION)
\r
253 def _on_close(self, *args):
\r
255 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
256 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
257 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
258 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
259 # push changes back to Hooke.config?
\r
260 self._c['manager'].UnInit()
\r
261 del self._c['manager']
\r
268 def _command_by_name(self, name):
\r
269 cs = [c for c in self.commands if c.name == name]
\r
271 raise KeyError(name)
\r
273 raise Exception('Multiple commands named "%s"' % name)
\r
276 def execute_command(self, _class=None, method=None,
\r
277 command=None, args=None):
\r
280 if ('property' in self._c
\r
281 and self.gui.config['selected command'] == command):
\r
282 arg_names = [arg.name for arg in command.arguments]
\r
283 for name,value in self._c['property'].get_values().items():
\r
284 if name in arg_names:
\r
286 print 'executing', command.name, args
\r
287 self.inqueue.put(CommandMessage(command, args))
\r
290 msg = self.outqueue.get()
\r
291 results.append(msg)
\r
292 if isinstance(msg, Exit):
\r
295 elif isinstance(msg, CommandExit):
\r
296 # TODO: display command complete
\r
298 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
299 self.gui.reload_config(msg.config)
\r
301 elif isinstance(msg, Request):
\r
302 h = handler.HANDLERS[msg.type]
\r
303 h.run(self, msg) # TODO: pause for response?
\r
306 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
307 self._postprocess_text)
\r
308 pp(command=command, results=results)
\r
311 def _handle_request(self, msg):
\r
312 """Repeatedly try to get a response to `msg`.
\r
315 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
316 prompt_string = prompt(msg)
\r
317 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
319 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
323 self.cmd.stdout.write(''.join([
\r
324 error.__class__.__name__, ': ', str(error), '\n']))
\r
325 self.cmd.stdout.write(prompt_string)
\r
326 value = parser(msg, self.cmd.stdin.readline())
\r
328 response = msg.response(value)
\r
330 except ValueError, error:
\r
332 self.inqueue.put(response)
\r
336 # Command-specific postprocessing
\r
338 def _postprocess_text(self, command, results):
\r
339 """Print the string representation of the results to the Results window.
\r
341 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
342 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
343 doesn't print some internally handled messages
\r
344 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
346 for result in results:
\r
347 if isinstance(result, CommandExit):
\r
348 self._c['output'].write(result.__class__.__name__+'\n')
\r
349 self._c['output'].write(str(result).rstrip()+'\n')
\r
351 def _postprocess_load_playlist(self, command, results):
\r
352 """Update `self` to show the playlist.
\r
354 if not isinstance(results[-1], Success):
\r
355 self._postprocess_text(command, results)
\r
356 assert len(results) == 2, results
\r
357 playlist = results[0]
\r
359 self._c['playlists']._c['tree'].add_playlist(playlist)
\r
361 def _postprocess_get_playlist(self, command, results):
\r
362 if not isinstance(results[-1], Success):
\r
363 self._postprocess_text(command, results)
\r
364 assert len(results) == 2, results
\r
365 playlist = results[0]
\r
367 self._c['playlists']._c['tree'].update_playlist(playlist)
\r
369 def _postprocess_get_curve(self, command, results):
\r
370 """Update `self` to show the curve.
\r
372 if not isinstance(results[-1], Success):
\r
373 self._postprocess_text(command, results)
\r
374 assert len(results) == 2, results
\r
376 playlist = self._c['playlists']._c['tree'].get_selected_playlist()
\r
377 if playlist != None: # TODO: fix once we have hooke.plugin.playlists
\r
378 self._c['playlists']._c['tree'].set_selected_curve(
\r
381 def _postprocess_next_curve(self, command, results):
\r
382 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
386 def _postprocess_previous_curve(self, command, results):
\r
387 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
394 def _GetActiveFileIndex(self):
\r
395 lib.playlist.Playlist = self.GetActivePlaylist()
\r
396 #get the selected item from the tree
\r
397 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
398 #test if a playlist or a curve was double-clicked
\r
399 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
403 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
404 while selected_item.IsOk():
\r
406 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
409 def _GetPlaylistTab(self, name):
\r
410 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
411 if page.caption == name:
\r
415 def select_plugin(self, _class=None, method=None, plugin=None):
\r
418 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
420 playlist = lib.playlist.Playlist(self, self.drivers)
\r
422 playlist.add_curve(item)
\r
423 if playlist.count > 0:
\r
424 playlist.name = self._GetUniquePlaylistName(name)
\r
426 self.AddTayliss(playlist)
\r
428 def AppliesPlotmanipulator(self, name):
\r
430 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
431 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
433 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
435 def ApplyPlotmanipulators(self, plot, plot_file):
\r
437 Apply all active plotmanipulators.
\r
439 if plot is not None and plot_file is not None:
\r
440 manipulated_plot = copy.deepcopy(plot)
\r
441 for plotmanipulator in self.plotmanipulators:
\r
442 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
443 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
444 return manipulated_plot
\r
446 def GetActiveFigure(self):
\r
447 playlist_name = self.GetActivePlaylistName()
\r
448 figure = self.playlists[playlist_name].figure
\r
449 if figure is not None:
\r
453 def GetActiveFile(self):
\r
454 playlist = self.GetActivePlaylist()
\r
455 if playlist is not None:
\r
456 return playlist.get_active_file()
\r
459 def GetActivePlot(self):
\r
460 playlist = self.GetActivePlaylist()
\r
461 if playlist is not None:
\r
462 return playlist.get_active_file().plot
\r
465 def GetDisplayedPlot(self):
\r
466 plot = copy.deepcopy(self.displayed_plot)
\r
468 #plot.curves = copy.deepcopy(plot.curves)
\r
471 def GetDisplayedPlotCorrected(self):
\r
472 plot = copy.deepcopy(self.displayed_plot)
\r
474 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
477 def GetDisplayedPlotRaw(self):
\r
478 plot = copy.deepcopy(self.displayed_plot)
\r
480 plot.curves = copy.deepcopy(plot.raw_curves)
\r
483 def GetDockArt(self):
\r
484 return self._c['manager'].GetArtProvider()
\r
486 def GetPlotmanipulator(self, name):
\r
488 Returns a plot manipulator function from its name
\r
490 for plotmanipulator in self.plotmanipulators:
\r
491 if plotmanipulator.name == name:
\r
492 return plotmanipulator
\r
495 def HasPlotmanipulator(self, name):
\r
497 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
499 for plotmanipulator in self.plotmanipulators:
\r
500 if plotmanipulator.command == name:
\r
505 def _on_dir_ctrl_left_double_click(self, event):
\r
506 file_path = self.panelFolders.GetPath()
\r
507 if os.path.isfile(file_path):
\r
508 if file_path.endswith('.hkp'):
\r
509 self.do_loadlist(file_path)
\r
512 def _on_erase_background(self, event):
\r
515 def _on_notebook_page_close(self, event):
\r
516 ctrl = event.GetEventObject()
\r
517 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
518 self.DeleteFromPlaylists(playlist_name)
\r
520 def OnPaneClose(self, event):
\r
523 def OnPropGridChanged (self, event):
\r
524 prop = event.GetProperty()
\r
526 item_section = self.panelProperties.SelectedTreeItem
\r
527 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
528 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
529 config = self.gui.config[plugin]
\r
530 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
531 property_key = prop.GetName()
\r
532 property_value = prop.GetDisplayedString()
\r
534 config[property_section][property_key]['value'] = property_value
\r
536 def OnResultsCheck(self, index, flag):
\r
537 results = self.GetActivePlot().results
\r
538 if results.has_key(self.results_str):
\r
539 results[self.results_str].results[index].visible = flag
\r
540 results[self.results_str].update()
\r
544 def _on_size(self, event):
\r
547 def OnUpdateNote(self, event):
\r
549 Saves the note to the active file.
\r
551 active_file = self.GetActiveFile()
\r
552 active_file.note = self.panelNote.Editor.GetValue()
\r
554 def UpdateNote(self):
\r
555 #update the note for the active file
\r
556 active_file = self.GetActiveFile()
\r
557 if active_file is not None:
\r
558 self.panelNote.Editor.SetValue(active_file.note)
\r
560 def UpdatePlaylistsTreeSelection(self):
\r
561 playlist = self.GetActivePlaylist()
\r
562 if playlist is not None:
\r
563 if playlist.index >= 0:
\r
564 self._c['status bar'].set_playlist(playlist)
\r
568 def UpdatePlot(self, plot=None):
\r
570 def add_to_plot(curve, set_scale=True):
\r
571 if curve.visible and curve.x and curve.y:
\r
572 #get the index of the subplot to use as destination
\r
573 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
574 #set all parameters for the plot
\r
575 axes_list[destination].set_title(curve.title)
\r
577 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
578 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
579 #set the formatting details for the scale
\r
580 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
581 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
582 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
583 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
584 if curve.style == 'plot':
\r
585 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
586 if curve.style == 'scatter':
\r
587 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
588 #add the legend if necessary
\r
590 axes_list[destination].legend()
\r
593 active_file = self.GetActiveFile()
\r
594 if not active_file.driver:
\r
595 #the first time we identify a file, the following need to be set
\r
596 active_file.identify(self.drivers)
\r
597 for curve in active_file.plot.curves:
\r
598 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
599 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
600 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
601 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
602 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
603 if active_file.driver is None:
\r
604 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
606 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
607 #add raw curves to plot
\r
608 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
609 #apply all active plotmanipulators
\r
610 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
611 #add corrected curves to plot
\r
612 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
615 self.displayed_plot = copy.deepcopy(plot)
\r
617 figure = self.GetActiveFigure()
\r
620 #use '0' instead of e.g. '0.00' for scales
\r
621 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
622 #optionally remove the extension from the title of the plot
\r
623 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
624 if hide_curve_extension:
\r
625 title = lh.remove_extension(self.displayed_plot.title)
\r
627 title = self.displayed_plot.title
\r
628 figure.suptitle(title, fontsize=14)
\r
629 #create the list of all axes necessary (rows and columns)
\r
631 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
632 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
633 for index in range(number_of_rows * number_of_columns):
\r
634 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
636 #add all curves to the corresponding plots
\r
637 for curve in self.displayed_plot.curves:
\r
640 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
641 figure.subplots_adjust(hspace=0.3)
\r
644 self.panelResults.ClearResults()
\r
645 if self.displayed_plot.results.has_key(self.results_str):
\r
646 for curve in self.displayed_plot.results[self.results_str].results:
\r
647 add_to_plot(curve, set_scale=False)
\r
648 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
650 self.panelResults.ClearResults()
\r
652 figure.canvas.draw()
\r
654 def _on_curve_select(self, playlist, curve):
\r
655 #create the plot tab and add playlist to the dictionary
\r
656 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
657 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
658 #tab_index = self._c['notebook'].GetSelection()
\r
659 playlist.figure = plotPanel.get_figure()
\r
660 self.playlists[playlist.name] = playlist
\r
661 #self.playlists[playlist.name] = [playlist, figure]
\r
662 self._c['status bar'].set_playlist(playlist)
\r
667 def _on_playlist_left_doubleclick(self):
\r
668 index = self._c['notebook'].GetSelection()
\r
669 current_playlist = self._c['notebook'].GetPageText(index)
\r
670 if current_playlist != playlist_name:
\r
671 index = self._GetPlaylistTab(playlist_name)
\r
672 self._c['notebook'].SetSelection(index)
\r
673 self._c['status bar'].set_playlist(playlist)
\r
677 def _on_playlist_delete(self, playlist):
\r
678 notebook = self.Parent.plotNotebook
\r
679 index = self.Parent._GetPlaylistTab(playlist.name)
\r
680 notebook.SetSelection(index)
\r
681 notebook.DeletePage(notebook.GetSelection())
\r
682 self.Parent.DeleteFromPlaylists(playlist_name)
\r
686 # Command panel interface
\r
688 def select_command(self, _class, method, command):
\r
689 #self.select_plugin(plugin=command.plugin)
\r
690 if 'assistant' in self._c:
\r
691 self._c['assitant'].ChangeValue(command.help)
\r
692 self._c['property'].clear()
\r
693 for argument in command.arguments:
\r
694 if argument.name == 'help':
\r
696 p = prop_from_argument(
\r
697 argument, curves=[], playlists=[]) # TODO: lookup playlists/curves
\r
699 continue # property intentionally not handled (yet)
\r
700 self._c['property'].append_property(p)
\r
702 self.gui.config['selected command'] = command # TODO: push to engine
\r
706 # Playlist panel interface
\r
708 def _on_user_delete_playlist(self, _class, method, playlist):
\r
711 def _on_delete_playlist(self, _class, method, playlist):
\r
712 if hasattr(playlist, 'path') and playlist.path != None:
\r
713 os.remove(playlist.path)
\r
715 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
718 def _on_delete_curve(self, _class, method, playlist, curve):
\r
719 os.remove(curve.path)
\r
721 def _on_set_selected_playlist(self, _class, method, playlist):
\r
722 """TODO: playlists plugin with `jump to playlist`.
\r
726 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
727 """Call the `jump to curve` command.
\r
729 TODO: playlists plugin.
\r
731 # TODO: jump to playlist, get playlist
\r
732 index = playlist.index(curve)
\r
733 results = self.execute_command(
\r
734 command=self._command_by_name('jump to curve'),
\r
735 args={'index':index})
\r
736 if not isinstance(results[-1], Success):
\r
738 #results = self.execute_command(
\r
739 # command=self._command_by_name('get playlist'))
\r
740 #if not isinstance(results[-1], Success):
\r
742 self.execute_command(
\r
743 command=self._command_by_name('get curve'))
\r
749 def _next_curve(self, *args):
\r
750 """Call the `next curve` command.
\r
752 results = self.execute_command(
\r
753 command=self._command_by_name('next curve'))
\r
754 if isinstance(results[-1], Success):
\r
755 self.execute_command(
\r
756 command=self._command_by_name('get curve'))
\r
758 def _previous_curve(self, *args):
\r
759 """Call the `previous curve` command.
\r
761 results = self.execute_command(
\r
762 command=self._command_by_name('previous curve'))
\r
763 if isinstance(results[-1], Success):
\r
764 self.execute_command(
\r
765 command=self._command_by_name('get curve'))
\r
769 # Panel display handling
\r
771 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
772 pane = self._c['manager'].GetPane(panel_name)
\r
775 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
776 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
777 #folders_size = pane.GetSize()
\r
778 self.panelFolders.Fit()
\r
779 self._c['manager'].Update()
\r
781 def _setup_perspectives(self):
\r
782 """Add perspectives to menubar and _perspectives.
\r
784 self._perspectives = {
\r
785 'Default': self._c['manager'].SavePerspective(),
\r
787 path = self.gui.config['perspective path']
\r
788 if os.path.isdir(path):
\r
789 files = sorted(os.listdir(path))
\r
790 for fname in files:
\r
791 name, extension = os.path.splitext(fname)
\r
792 if extension != self.gui.config['perspective extension']:
\r
794 fpath = os.path.join(path, fname)
\r
795 if not os.path.isfile(fpath):
\r
798 with open(fpath, 'rU') as f:
\r
799 perspective = f.readline()
\r
801 self._perspectives[name] = perspective
\r
803 selected_perspective = self.gui.config['active perspective']
\r
804 if not self._perspectives.has_key(selected_perspective):
\r
805 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
807 self._restore_perspective(selected_perspective)
\r
808 self._update_perspective_menu()
\r
810 def _update_perspective_menu(self):
\r
811 self._c['menu bar']._c['perspective'].update(
\r
812 sorted(self._perspectives.keys()),
\r
813 self.gui.config['active perspective'])
\r
815 def _save_perspective(self, perspective, perspective_dir, name,
\r
817 path = os.path.join(perspective_dir, name)
\r
818 if extension != None:
\r
820 if not os.path.isdir(perspective_dir):
\r
821 os.makedirs(perspective_dir)
\r
822 with open(path, 'w') as f:
\r
823 f.write(perspective)
\r
824 self._perspectives[name] = perspective
\r
825 self._restore_perspective(name)
\r
826 self._update_perspective_menu()
\r
828 def _delete_perspectives(self, perspective_dir, names,
\r
832 path = os.path.join(perspective_dir, name)
\r
833 if extension != None:
\r
836 del(self._perspectives[name])
\r
837 self._update_perspective_menu()
\r
838 if self.gui.config['active perspective'] in names:
\r
839 self._restore_perspective('Default')
\r
840 # TODO: does this bug still apply?
\r
841 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
842 # http://trac.wxwidgets.org/ticket/3258
\r
843 # ) that makes the radio item indicator in the menu disappear.
\r
844 # The code should be fine once this issue is fixed.
\r
846 def _restore_perspective(self, name):
\r
847 if name != self.gui.config['active perspective']:
\r
848 print 'restoring perspective:', name
\r
849 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
850 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
851 self._c['manager'].Update()
\r
852 for pane in self._c['manager'].GetAllPanes():
\r
853 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
854 pane.Check(pane.window.IsShown())
\r
856 def _on_save_perspective(self, *args):
\r
857 perspective = self._c['manager'].SavePerspective()
\r
858 name = self.gui.config['active perspective']
\r
859 if name == 'Default':
\r
860 name = 'New perspective'
\r
861 name = select_save_file(
\r
862 directory=self.gui.config['perspective path'],
\r
864 extension=self.gui.config['perspective extension'],
\r
866 message='Enter a name for the new perspective:',
\r
867 caption='Save perspective')
\r
870 self._save_perspective(
\r
871 perspective, self.gui.config['perspective path'], name=name,
\r
872 extension=self.gui.config['perspective extension'])
\r
874 def _on_delete_perspective(self, *args, **kwargs):
\r
875 options = sorted([p for p in self._perspectives.keys()
\r
876 if p != 'Default'])
\r
877 dialog = SelectionDialog(
\r
879 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
880 button_id=wx.ID_DELETE,
\r
881 selection_style='multiple',
\r
883 title='Delete perspective(s)',
\r
884 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
885 dialog.CenterOnScreen()
\r
887 names = [options[i] for i in dialog.selected]
\r
889 self._delete_perspectives(
\r
890 self.gui.config['perspective path'], names=names,
\r
891 extension=self.gui.config['perspective extension'])
\r
893 def _on_select_perspective(self, _class, method, name):
\r
894 self._restore_perspective(name)
\r
898 class HookeApp (wx.App):
\r
899 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
901 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
904 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
906 self.commands = commands
\r
907 self.inqueue = inqueue
\r
908 self.outqueue = outqueue
\r
909 super(HookeApp, self).__init__(*args, **kwargs)
\r
912 self.SetAppName('Hooke')
\r
913 self.SetVendorName('')
\r
914 self._setup_splash_screen()
\r
916 height = int(self.gui.config['main height']) # HACK: config should convert
\r
917 width = int(self.gui.config['main width'])
\r
918 top = int(self.gui.config['main top'])
\r
919 left = int(self.gui.config['main left'])
\r
921 # Sometimes, the ini file gets confused and sets 'left' and
\r
922 # 'top' to large negative numbers. Here we catch and fix
\r
923 # this. Keep small negative numbers, the user might want
\r
931 'frame': HookeFrame(
\r
932 self.gui, self.commands, self.inqueue, self.outqueue,
\r
933 parent=None, title='Hooke',
\r
934 pos=(left, top), size=(width, height),
\r
935 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
937 self._c['frame'].Show(True)
\r
938 self.SetTopWindow(self._c['frame'])
\r
941 def _setup_splash_screen(self):
\r
942 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
943 print 'splash', self.gui.config['show splash screen']
\r
944 path = self.gui.config['splash screen image']
\r
945 if os.path.isfile(path):
\r
946 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
948 bitmap=wx.Image(path).ConvertToBitmap(),
\r
949 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
950 milliseconds=duration,
\r
953 # For some reason splashDuration and sleep do not
\r
954 # correspond to each other at least not on Windows.
\r
955 # Maybe it's because duration is in milliseconds and
\r
956 # sleep in seconds. Thus we need to increase the
\r
957 # sleep time a bit. A factor of 1.2 seems to work.
\r
959 time.sleep(sleepFactor * duration / 1000)
\r
962 class GUI (UserInterface):
\r
963 """wxWindows graphical user interface.
\r
965 def __init__(self):
\r
966 super(GUI, self).__init__(name='gui')
\r
968 def default_settings(self):
\r
969 """Return a list of :class:`hooke.config.Setting`\s for any
\r
970 configurable UI settings.
\r
972 The suggested section setting is::
\r
974 Setting(section=self.setting_section, help=self.__doc__)
\r
977 Setting(section=self.setting_section, help=self.__doc__),
\r
978 Setting(section=self.setting_section, option='icon image',
\r
979 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
980 help='Path to the hooke icon image.'),
\r
981 Setting(section=self.setting_section, option='show splash screen',
\r
983 help='Enable/disable the splash screen'),
\r
984 Setting(section=self.setting_section, option='splash screen image',
\r
985 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
986 help='Path to the Hooke splash screen image.'),
\r
987 Setting(section=self.setting_section, option='splash screen duration',
\r
989 help='Duration of the splash screen in milliseconds.'),
\r
990 Setting(section=self.setting_section, option='perspective path',
\r
991 value=os.path.join('resources', 'gui', 'perspective'),
\r
992 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
993 Setting(section=self.setting_section, option='perspective extension',
\r
995 help='Extension for perspective files.'),
\r
996 Setting(section=self.setting_section, option='hide extensions',
\r
998 help='Hide file extensions when displaying names.'),
\r
999 Setting(section=self.setting_section, option='folders-workdir',
\r
1001 help='This should probably go...'),
\r
1002 Setting(section=self.setting_section, option='folders-filters',
\r
1004 help='This should probably go...'),
\r
1005 Setting(section=self.setting_section, option='active perspective',
\r
1007 help='Name of active perspective file (or "Default").'),
\r
1008 Setting(section=self.setting_section, option='folders-filter-index',
\r
1010 help='This should probably go...'),
\r
1011 Setting(section=self.setting_section, option='main height',
\r
1013 help='Height of main window in pixels.'),
\r
1014 Setting(section=self.setting_section, option='main width',
\r
1016 help='Width of main window in pixels.'),
\r
1017 Setting(section=self.setting_section, option='main top',
\r
1019 help='Pixels from screen top to top of main window.'),
\r
1020 Setting(section=self.setting_section, option='main left',
\r
1022 help='Pixels from screen left to left of main window.'),
\r
1023 Setting(section=self.setting_section, option='selected command',
\r
1024 value='load playlist',
\r
1025 help='Name of the initially selected command.'),
\r
1028 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1032 app = HookeApp(gui=self,
\r
1033 commands=commands,
\r
1034 inqueue=ui_to_command_queue,
\r
1035 outqueue=command_to_ui_queue,
\r
1036 redirect=redirect)
\r
1039 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1040 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r